1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-01 22:22:59 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Jonathan Coates
f26e443e81 Bump CC:T to 1.109.5 2024-01-31 20:53:28 +00:00
Jonathan Coates
033378333f Standardise how we discard "char" events
One common issue we get when a program exits after handling a "key"
event is that it leaves the "char" event on the queue. This means that
the shell (or whatever program we switch in to) then receives the "char"
event, often displaying it to the screen.

Previously we've got around this by doing sleep(0) before exiting the
program. However, we also see this problem in edit's run handler script,
and I'm less comfortable doing the same hack there.

This adds a new internal discard_char function, which will either
wait one tick or return when seeing a char/key_up event.

Fixes #1705
2024-01-31 20:49:43 +00:00
Jonathan Coates
ebeaa757a9 Change how we put test libraries on the class path
- Mark our core test-fixtures jar as part of the "cctest", rather than
   a separate library. I'm fairly sure this was actually using the
   classpath version of CC rather than the legacyClasspath version!

 - Add a new "testMinecraftLibrary" configuration, instead of trying to
   infer it from the classpath. We have to jump through some hoops to
   avoid having multiple versions of a library on the classpath at once,
   but it's not too bad.

I'm working on a patch to bsl which might allow us to kill of
legacyClasspath instead. Please, anything is better than this.
2024-01-31 19:49:36 +00:00
Jonathan Coates
57b1a65db3 Fix using a tab instead of space
Annoying that pre-commit didn't catch this!
2024-01-30 22:01:57 +00:00
Jonathan Coates
27c72a4571 Use client-side commands for opening computer folders
Forge doesn't run client-side commands from sendUnsignedCommand, so we
still require a mixin there.

We do need to change the command name, as Fabric doesn't properly merge
the two command trees.
2024-01-30 22:00:36 +00:00
Jonathan Coates
f284328656 Regenerate Gradle wrapper 2024-01-29 22:14:48 +00:00
Jonathan Coates
6b83c63991 Switch to our own Gradle plugin for vanilla Minecraft
I didn't make a new years resolution to stop writing build tooling, but
maybe I should have.

This replaces our use of VanillaGradle with a new project,
VanillaExtract. This offers a couple of useful features for multi-loader
dev, including Parchment and Unpick support, both of which we now use in
CC:T.
2024-01-29 20:59:16 +00:00
Jonathan Coates
b27526bd21 Bump CC:T to 1.109.4 2024-01-27 10:26:56 +00:00
Jonathan Coates
cb25f6c08a Update Cobalt to 0.9.0
- Debug hooks are now correctly called for every function.
 - Fix several minor inconsistencies with debug.getinfo.
 - Fix Lua tables being sized incorrectly when created from varargs.
2024-01-27 10:04:34 +00:00
Jonathan Coates
d38b1d04e7 Update Gradle to 8.5
- Update FG to 6.0.20 - no major changes, but required for the Gradle
   update.
 - Update Loom to 1.5.x - this adds Vineflower support by default, so we
   can remove loom-vineflower.
2024-01-27 09:28:13 +00:00
Jonathan Coates
9ccee75a99 Fix the docs for ReadHandle.read's "count"
This was copied over from the old binary handle, and so states we
always return a single number if no count is given. This is only the
case when the file is opened in binary mode.
2024-01-23 22:39:49 +00:00
Jonathan Coates
359c8d6652 Reformat JSON by wrapping CachedOutput
Rather than mixing-in to CachedOutput, we just wrap our DataProviders to
use a custom CachedOutput which reformats the JSON before writing. This
allows us to drop mixins for common+non-client code.
2024-01-21 17:50:59 +00:00
Jonathan Coates
1788afacfc Remove note about disabling websocket limits
I suspect this was copied from the file limit, which can be turned off
by setting to 0.

Fixes #1691
2024-01-21 16:32:07 +00:00
Jonathan Coates
f695f22d8a Atomic update of disk drive item stacks
Disk drives have had a long-standing issue with mutating their contents
on the computer thread, potentially leading to all sorts of odd bugs.

We tried to fix this by moving setDiskLabel and the mounting code to run
on the main thread. Unfortunately, this means there is a slight delay to
mounts being attached, breaking disk startup.

This commit implements an alternative solution - we now do mounting on
the computer thread again. If the disk's stack is modified, we update it
in the peripheral-facing item, but not the actual inventory. The next
time the disk drive is ticked, we then sync the two items.

This does mean that there is a fraction of a tick where the two will be
out-of-sync. This isn't ideal - it would potentially be possible to
cycle through disk ids - but I don't really think that's avoidable
without significantly complicating the IMedia API.

Fixes #1649, fixes #1686.
2024-01-20 18:46:43 +00:00
Jonathan Coates
bc03090ca4 Merge pull request #1684 from cc-tweaked/hotfix/turtle-modellers-redo
Rewrite turtle upgrade modeller registration API
2024-01-17 19:45:48 +00:00
Jonathan Coates
a617d0d566 Rewrite turtle upgrade modeller registration API
Originally we exposed a single registerTurtleUpgradeModellermethod which
could be called from both Fabric (during a mod's client init) and Forge
(during FMLClientSetupEvent).

This was fine until we allowed upgrades to specify model dependencies,
which would then automatically loaded, as this means model loading now
depends on upgrade modellers being loaded. Unknown to me, this is not
guaranteed to be the case on Forge - mod setup happens at the same time
as resource reloading!

Unfortunately there's not really a salvageable way of fixing this with
the current API. Forge now uses a registration event-based system,
meaning we can guarantee all modellers are loaded before models are
baked.
2024-01-16 23:00:49 +00:00
Jonathan Coates
36599b321e Backport small changes from the 1.20.4 branch
- Add support for version overrides/exclusions in our dependency check.
   Sometimes mod loaders use different versions to vanilla, and we need
   some way to handle that.

 - Rescan wired network connections on the tick after invalidation,
   rather than when invalidated.

 - Convert some constant lambdas to static method references. Lambdas
   don't allocate if they don't capture variables, so this has the same
   performance and is a little less ugly.

 - Small code-style/formatting changes.
2024-01-16 21:42:25 +00:00
Jonathan Coates
1d6e3f4fc0 Change ComponentLookup to use ServerLevel
Makes this more consistent with the rest of the peripheral code, and our
changes in 1.20.4.
2024-01-15 08:28:59 +00:00
Jonathan Coates
30dc4cb38c Simplify our networking multi-platform code
Historically we used Forge's SimpleChannel methods (and
PacketDistributor) to send the packets to the client. However, we don't
need to do that - it is sufficient to convert it to a vanilla packet,
and send the packet ourselves.

Given we need to do this on Fabric, it makes sense to do this on Forge
as well. This allows us to unify (and thus simplify) a lot of how packet
sending works.

At the same time, we also remove the handling of speaker audio during
decoding. We originally did this to avoid the additional copy of audio
data. However, this doesn't work on 1.20.4 (as packets aren't
encoded/decoded on singleplayer), so it makes sense to do this
Correctly(TM).

This also allows us to get rid of ClientNetworkContext.get(). We do
still need to service load this class (as Forge's networking isn't split
up in the same way Fabric's is), but we'll be able to drop that in
1.20.4.

Finally, we move the record playing code from ClientNetworkContext to
ClientPlatformHelper. This means the network context no longer needs to
be platform-specific!
2024-01-14 22:53:36 +00:00
Jonathan Coates
be4512d1c3 Construct ComponentAccesses with the BE
After embarrassing, let's do some proper work.

Rather than passing the level and position each time we call
ComponentAccess.get(), we now pass them at construction time (in the
form of the BE). This makes the consuming code a little cleaner, and is
required for the NeoForge changes in 1.20.4.
2024-01-14 17:46:37 +00:00
Jonathan Coates
e6ee292850 Fix incorrect "Fix incorrect "incorrect incorrect"" 2024-01-14 16:27:55 +00:00
Jonathan Coates
9d36f72bad Fix incorrect "incorrect incorrect" 2024-01-14 16:12:52 +00:00
Jonathan Coates
b5923c4462 Flesh out the printer documentation slightly 2024-01-14 12:25:04 +00:00
Jonathan Coates
4d1e689719 Fix endPage() not updating the printer block state
This meant that we didn't show the bottom slot was full until other
items were moved in the inventory.
2024-01-14 12:23:55 +00:00
Marcus
9d4af07568 fix: breaking_changes flattening link incorrect (#1679) 2024-01-10 22:18:22 +00:00
lonevox
89294f4a22 Fix incorrect Lua list indexes in NBT tags (#1678) 2024-01-10 19:16:15 +00:00
Jonathan Coates
133b51b092 Don't warn when allocating 0 bytes
I was able to reproduce this by starting two computers, and then warming
up the JIT by running:

  while true do os.queueEvent("x") os.pullEvent("x") end

and then running the following on one computer, while typing on the
other:

  while true do end

I'm not quite sure why this happens. It's possible that once the JIT is
warm, we can resume computers without actually allocating anything,
though I'm a little unconvinced.

Fixes #1672
2024-01-08 21:52:30 +00:00
Jonathan Coates
272010e945 Require Minecraft 1.20.1
Closes #1671
2024-01-08 21:33:55 +00:00
Jonathan Coates
e0889c613a Mark "check valid item" test as required
This has passed for years now, no reason for it to be optional.
2024-01-07 13:35:38 +00:00
Jonathan Coates
f115d43d07 Fix some dependencies not appearing in the POM
Again! This time it was just the night-config ones.
2024-01-03 21:05:03 +00:00
117 changed files with 2464 additions and 1331 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
/logs
/build
/projects/*/logs
/projects/fabric/fabricloader.log
/projects/*/build
/buildSrc/build
/out

View File

@@ -75,8 +75,8 @@ minecraft {
```
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into
exposing more features.
subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file
an issue to let me know!
We bundle the API sources with the jar, so documentation should be easily viewable within your editor. Alternatively,
the generated documentation [can be browsed online](https://tweaked.cc/javadoc/).

View File

@@ -29,19 +29,19 @@ repositories {
}
}
maven("https://repo.spongepowered.org/repository/maven-public/") {
name = "Sponge"
content {
includeGroup("org.spongepowered")
}
}
maven("https://maven.fabricmc.net/") {
name = "Fabric"
content {
includeGroup("net.fabricmc")
}
}
maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
includeGroup("cc.tweaked.vanilla-extract")
}
}
}
dependencies {
@@ -55,8 +55,7 @@ dependencies {
implementation(libs.ideaExt)
implementation(libs.librarian)
implementation(libs.minotaur)
implementation(libs.vanillaGradle)
implementation(libs.vineflower)
implementation(libs.vanillaExtract)
}
gradlePlugin {

View File

@@ -12,7 +12,6 @@ import cc.tweaked.gradle.MinecraftConfigurations
plugins {
`java-library`
id("fabric-loom")
id("io.github.juuxel.loom-vineflower")
id("cc-tweaked.java-convention")
}

View File

@@ -10,25 +10,31 @@ import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("cc-tweaked.java-convention")
id("org.spongepowered.gradle.vanilla")
id("cc.tweaked.vanilla-extract")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
minecraft {
version(mcVersion)
mappings {
parchment(libs.findVersion("parchmentMc").get().toString(), libs.findVersion("parchment").get().toString())
}
unpick(libs.findLibrary("yarn").get())
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
// Depend on error prone annotations to silence a lot of compile warnings.
compileOnlyApi(libs.findLibrary("errorProne.annotations").get())
compileOnly(libs.findLibrary("errorProne.annotations").get())
}
MinecraftConfigurations.setup(project)
MinecraftConfigurations.setupBasic(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = null)

View File

@@ -255,7 +255,7 @@ abstract class CCTweakedExtension(
}
/**
* Exclude a dependency from being publisehd in Maven.
* Exclude a dependency from being published in Maven.
*/
fun exclude(dep: Dependency) {
excludedDeps.add(dep)

View File

@@ -7,12 +7,15 @@ 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.MinimalExternalModuleDependency
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.provider.MapProperty
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin
@@ -21,9 +24,25 @@ abstract class DependencyCheck : DefaultTask() {
@get:Input
abstract val configuration: ListProperty<Configuration>
/**
* A mapping of module coordinates (`group:module`) to versions, overriding the requested version.
*/
@get:Input
abstract val overrides: MapProperty<String, String>
init {
description = "Check :core's dependencies are consistent with Minecraft's."
group = LifecycleBasePlugin.VERIFICATION_GROUP
configuration.finalizeValueOnRead()
overrides.finalizeValueOnRead()
}
/**
* Override a module with a different version.
*/
fun override(module: Provider<MinimalExternalModuleDependency>, version: String) {
overrides.putAll(project.provider { mutableMapOf(module.get().module.toString() to version) })
}
@TaskAction
@@ -60,7 +79,8 @@ abstract class DependencyCheck : DefaultTask() {
) {
// If the version is different between the requested and selected version, report an error.
val selected = dependency.selected.moduleVersion!!.version
if (requested.version != selected) {
val requestedVersion = overrides.get()["${requested.group}:${requested.module}"] ?: requested.version
if (requestedVersion != selected) {
logger.error("Requested dependency {} (via {}) but got version {}", requested, from, selected)
return false
}

View File

@@ -4,23 +4,17 @@
package cc.tweaked.gradle
import cc.tweaked.vanillaextract.configurations.Capabilities
import cc.tweaked.vanillaextract.configurations.MinecraftSetup
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.java.TargetJvmVersion
import org.gradle.api.capabilities.Capability
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.named
/**
* This sets up a separate client-only source set, and extends that and the main/common source set with additional
@@ -59,31 +53,13 @@ class MinecraftConfigurations private constructor(private val project: Project)
}
configurations.named(client.implementationConfigurationName) { extendsFrom(clientApi) }
/*
Now add outgoing variants for the main and common source sets that we can consume downstream. This is possibly
the worst way to do things, but unfortunately the alternatives don't actually work very well:
- Just using source set outputs: This means dependencies don't propagate, which means when :fabric depends
on :fabric-api, we don't inherit the fake :common-api in IDEA.
- Having separate common/main jars: Nice in principle, but unfortunately Forge needs a separate deobf jar
task (as the original jar is obfuscated), and IDEA is not able to map its output back to a source set.
This works for now, but is incredibly brittle. It's part of the reason we can't use testFixtures inside our
MC projects, as that adds a project(self) -> test dependency, which would pull in the jar instead.
Note we register a fake client jar here. It's not actually needed, but is there to make sure IDEA has
a way to tell that client classes are needed at runtime.
I'm so sorry, deeply aware how cursed this is.
*/
setupOutgoing(main, "CommonOnly")
project.tasks.register(client.jarTaskName, Jar::class.java) {
description = "An empty jar standing in for the client classes."
group = BasePlugin.BUILD_GROUP
archiveClassifier.set("client")
}
setupOutgoing(client)
MinecraftSetup(project).setupOutgoingConfigurations()
// Reset the client classpath (Loom configures it slightly differently to this) and add a main -> client
// dependency. Here we /can/ use source set outputs as we add transitive deps by patching the classpath. Nasty,
@@ -106,6 +82,12 @@ class MinecraftConfigurations private constructor(private val project: Project)
project.tasks.named("jar", Jar::class.java) { from(client.output) }
project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) }
setupBasic()
}
private fun setupBasic() {
val client = sourceSets["client"]
project.extensions.configure(CCTweakedExtension::class.java) {
sourceDirectories.add(SourceSetReference.internal(client))
}
@@ -120,83 +102,19 @@ class MinecraftConfigurations private constructor(private val project: Project)
project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
}
private fun setupOutgoing(sourceSet: SourceSet, suffix: String = "") {
setupOutgoing("${sourceSet.apiElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_API)) {
description = "API elements for ${sourceSet.name}"
extendsFrom(configurations[sourceSet.apiConfigurationName])
}
setupOutgoing("${sourceSet.runtimeElementsConfigurationName}$suffix", sourceSet, objects.named(Usage.JAVA_RUNTIME)) {
description = "Runtime elements for ${sourceSet.name}"
extendsFrom(configurations[sourceSet.implementationConfigurationName], configurations[sourceSet.runtimeOnlyConfigurationName])
}
}
/**
* Set up an outgoing configuration for a specific source set. We set an additional "main" or "client" capability
* (depending on the source set name) which allows downstream projects to consume them separately (see
* [DependencyHandler.commonClasses] and [DependencyHandler.clientClasses]).
*/
private fun setupOutgoing(name: String, sourceSet: SourceSet, usage: Usage, configure: Configuration.() -> Unit) {
configurations.register(name) {
isVisible = false
isCanBeConsumed = true
isCanBeResolved = false
configure(this)
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, usage)
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
attributeProvider(
TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE,
java.toolchain.languageVersion.map { it.asInt() },
)
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
}
outgoing {
capability(BasicOutgoingCapability(project, sourceSet.name))
// We have two outgoing variants here: the original jar and the classes.
artifact(project.tasks.named(sourceSet.jarTaskName))
variants.create("classes") {
attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.CLASSES))
sourceSet.output.classesDirs.forEach { artifact(it) { builtBy(sourceSet.output) } }
}
}
}
}
companion object {
fun setupBasic(project: Project) {
MinecraftConfigurations(project).setupBasic()
}
fun setup(project: Project) {
MinecraftConfigurations(project).setup()
}
}
}
private class BasicIncomingCapability(private val module: ModuleDependency, private val name: String) : Capability {
override fun getGroup(): String = module.group!!
override fun getName(): String = "${module.name}-$name"
override fun getVersion(): String? = null
}
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency =
Capabilities.clientClasses(create(notation) as ModuleDependency)
private class BasicOutgoingCapability(private val project: Project, private val name: String) : Capability {
override fun getGroup(): String = project.group.toString()
override fun getName(): String = "${project.name}-$name"
override fun getVersion(): String = project.version.toString()
}
fun DependencyHandler.clientClasses(notation: Any): ModuleDependency {
val dep = create(notation) as ModuleDependency
dep.capabilities { requireCapability(BasicIncomingCapability(dep, "client")) }
return dep
}
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency {
val dep = create(notation) as ModuleDependency
dep.capabilities { requireCapability(BasicIncomingCapability(dep, "main")) }
return dep
}
fun DependencyHandler.commonClasses(notation: Any): ModuleDependency =
Capabilities.commonClasses(create(notation) as ModuleDependency)

View File

@@ -21,7 +21,7 @@ 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}
## CC: Tweaked 1.109.0 to 1.109.3 {#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.
@@ -76,6 +76,6 @@ as documentation for breaking changes and "gotchas" one should look out for betw
- Programs containing `/` are looked up in the current directory and are no longer looked up on the path. For instance,
you can no longer type `turtle/excavate` to run `/rom/programs/turtle/excavate.lua`.
[flattening]: https://minecraft.wiki.com/w/Java_Edition_1.13/Flattening
[flattening]: https://minecraft.wiki/w/Java_Edition_1.13/Flattening
[legal_data_pack]: https://minecraft.gamepedia.com/Tutorials/Creating_a_data_pack#Legal_characters
[datapack-example]: https://github.com/cc-tweaked/datapack-example "An example datapack for CC: Tweaked"

View File

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

View File

@@ -14,6 +14,7 @@ forgeSpi = "7.0.1"
mixin = "0.8.5"
parchment = "2023.08.20"
parchmentMc = "1.20.1"
yarn = "1.20.1+build.10"
# Core dependencies (these versions are tied to the version Minecraft uses)
fastutil = "8.5.9"
@@ -25,7 +26,7 @@ slf4j = "2.0.1"
asm = "9.6"
autoService = "1.1.1"
checkerFramework = "3.42.0"
cobalt = "0.8.2"
cobalt = "0.9.0"
commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2"
@@ -57,8 +58,8 @@ checkstyle = "10.12.6"
curseForgeGradle = "1.0.14"
errorProne-core = "2.23.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.3.9"
forgeGradle = "6.0.8"
fabric-loom = "1.5.7"
forgeGradle = "6.0.20"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
@@ -71,9 +72,8 @@ nullAway = "0.9.9"
spotless = "6.23.3"
taskTree = "2.1.1"
teavm = "0.10.0-SQUID.2"
vanillaGradle = "0.2.1-SNAPSHOT"
vanillaExtract = "0.1.1"
versionCatalogUpdate = "0.8.1"
vineflower = "1.11.0"
[libraries]
# Normal dependencies
@@ -159,8 +159,8 @@ teavm-metaprogramming-api = { module = "org.teavm:teavm-metaprogramming-api", ve
teavm-metaprogramming-impl = { module = "org.teavm:teavm-metaprogramming-impl", version.ref = "teavm" }
teavm-platform = { module = "org.teavm:teavm-platform", version.ref = "teavm" }
teavm-tooling = { module = "org.teavm:teavm-tooling", version.ref = "teavm" }
vanillaGradle = { module = "org.spongepowered:vanillagradle", version.ref = "vanillaGradle" }
vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vineflower" }
vanillaExtract = { module = "cc.tweaked.vanilla-extract:plugin", version.ref = "vanillaExtract" }
yarn = { module = "net.fabricmc:yarn", version.ref = "yarn" }
[plugins]
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
@@ -180,7 +180,6 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"]
externalMods-forge-compile = ["moreRed", "oculus", "jei-api"]
externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric = ["nightConfig-core", "nightConfig-toml"]
externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"]
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]

Binary file not shown.

View File

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

22
gradlew vendored
View File

@@ -83,7 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -130,10 +131,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -198,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

1376
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"rehype-highlight": "^7.0.0",
"rehype-react": "^8.0.0",
"rollup": "^4.0.0",
"tsx": "^3.12.10",
"tsx": "^4.7.0",
"typescript": "^5.2.2"
}
}

View File

@@ -27,8 +27,13 @@ public final class ComputerCraftAPIClient {
* @param serialiser The turtle upgrade serialiser.
* @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade.
* @deprecated This method can lead to confusing load behaviour on Forge. Use
* {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} on Fabric, or
* {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} on Forge.
*/
@Deprecated(forRemoval = true)
public static <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
// TODO(1.20.4): Remove this
getInstance().registerTurtleUpgradeModeller(serialiser, modeller);
}

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
/**
* A functional interface to register a {@link TurtleUpgradeModeller} for a class of turtle upgrades.
* <p>
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
* multiple loaders.
*/
@FunctionalInterface
public interface RegisterTurtleUpgradeModeller {
/**
* Register a {@link TurtleUpgradeModeller}.
*
* @param serialiser The turtle upgrade serialiser.
* @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade.
*/
<T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller);
}

View File

@@ -4,12 +4,10 @@
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.client.ComputerCraftAPIClient;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.nbt.CompoundTag;
@@ -21,9 +19,13 @@ import java.util.List;
/**
* Provides models for a {@link ITurtleUpgrade}.
* <p>
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
* on Forge
*
* @param <T> The type of turtle upgrade this modeller applies to.
* @see ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller) To register a modeller.
* @see RegisterTurtleUpgradeModeller For multi-loader registration support.
*/
public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
/**

View File

@@ -48,13 +48,8 @@ import java.util.function.Function;
* }
* }</pre>
* <p>
* Finally, we need to register a model for our upgrade. This is done with
* {@link dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller}:
*
* <pre>{@code
* // Register our model inside FMLClientSetupEvent
* ComputerCraftAPIClient.registerTurtleUpgradeModeller(MY_UPGRADE.get(), TurtleUpgradeModeller.flatItem())
* }</pre>
* Finally, we need to register a model for our upgrade. The way to do this varies on mod loader, see
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information.
* <p>
* {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
*

View File

@@ -22,6 +22,14 @@ configurations {
register("cctJavadoc")
}
repositories {
maven("https://maven.minecraftforge.net/") {
content {
includeModule("org.spongepowered", "mixin")
}
}
}
dependencies {
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
implementation(project(":core"))
@@ -31,7 +39,6 @@ dependencies {
compileOnly(libs.bundles.externalMods.common)
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
compileOnly(libs.mixin)
annotationProcessorEverywhere(libs.autoService)
testFixturesAnnotationProcessor(libs.autoService)
@@ -39,6 +46,7 @@ dependencies {
testImplementation(libs.bundles.test)
testRuntimeOnly(libs.bundles.testRuntime)
testModCompileOnly(libs.mixin)
testModImplementation(testFixtures(project(":core")))
testModImplementation(testFixtures(project(":common")))
testModImplementation(libs.bundles.kotlin)
@@ -72,7 +80,7 @@ val luaJavadoc by tasks.registering(Javadoc::class) {
javadocTool.set(
javaToolchains.javadocToolFor {
languageVersion.set(cc.tweaked.gradle.CCTweakedPlugin.JAVA_VERSION)
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
},
)
}

View File

@@ -17,8 +17,6 @@ import dan200.computercraft.client.render.monitor.MonitorRenderState;
import dan200.computercraft.client.sound.SpeakerManager;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
@@ -28,7 +26,6 @@ import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.PauseAwareTimer;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.Util;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
@@ -43,7 +40,6 @@ import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import javax.annotation.Nullable;
import java.io.File;
import java.util.function.Consumer;
/**
@@ -71,10 +67,6 @@ public final class ClientHooks {
ClientPocketComputers.reset();
}
public static boolean onChatMessage(String message) {
return handleOpenComputerCommand(message);
}
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit)
|| MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit);
@@ -109,34 +101,6 @@ public final class ClientHooks {
SpeakerManager.onPlayStreaming(engine, channel, stream);
}
/**
* Handle the {@link CommandComputerCraft#OPEN_COMPUTER} "clientside command". This isn't a true command, as we
* don't want it to actually be visible to the user.
*
* @param message The current chat message.
* @return Whether to cancel sending this message.
*/
private static boolean handleOpenComputerCommand(String message) {
if (!message.startsWith(CommandComputerCraft.OPEN_COMPUTER)) return false;
var server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) return false;
var idStr = message.substring(CommandComputerCraft.OPEN_COMPUTER.length()).trim();
int id;
try {
id = Integer.parseInt(idStr);
} catch (NumberFormatException ignore) {
return false;
}
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) return false;
Util.getPlatform().openFile(file);
return true;
}
/**
* Add additional information about the currently targeted block to the debug screen.
*

View File

@@ -4,8 +4,12 @@
package dan200.computercraft.client;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.ComputerCraftAPIClient;
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.pocket.ClientPocketComputers;
@@ -16,11 +20,14 @@ import dan200.computercraft.client.turtle.TurtleModemModeller;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemColor;
import net.minecraft.client.gui.screens.MenuScreens;
@@ -30,6 +37,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceProvider;
@@ -39,6 +47,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ItemLike;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -60,18 +69,6 @@ public final class ClientRegistry {
* Register any client-side objects which don't have to be done on the main thread.
*/
public static void register() {
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
ComputerCraftAPIClient.registerTurtleUpgradeModeller(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_NORMAL.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new);
@@ -103,6 +100,20 @@ public final class ClientRegistry {
);
}
public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
register.register(ModRegistry.TurtleSerialisers.SPEAKER.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
register.register(ModRegistry.TurtleSerialisers.WORKBENCH.get(), TurtleUpgradeModeller.sided(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
register.register(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_NORMAL.get(), new TurtleModemModeller(false));
register.register(ModRegistry.TurtleSerialisers.WIRELESS_MODEM_ADVANCED.get(), new TurtleModemModeller(true));
register.register(ModRegistry.TurtleSerialisers.TOOL.get(), TurtleUpgradeModeller.flatItem());
}
@SafeVarargs
private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
@@ -179,4 +190,45 @@ public final class ClientRegistry {
return function.unclampedCall(stack, level, entity, layer);
}
}
/**
* Register client-side commands.
*
* @param dispatcher The dispatcher to register the commands to.
* @param sendError A function to send an error message.
* @param <T> The type of the client-side command context.
*/
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
.then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
.executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
));
}
/**
* Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
*
* @param context The command context.
* @param sendError A function to send an error message.
* @param id The computer's id.
* @param <T> The type of the client-side command context.
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
*/
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
var server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) {
sendError.accept(context, Component.literal("Not on a single-player server"));
return 0;
}
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) {
sendError.accept(context, Component.literal("Computer's folder does not exist"));
return 0;
}
Util.getPlatform().openFile(file);
return 1;
}
}

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.InputHandler;
@@ -207,7 +207,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
return;
}
if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientPlatformHelper.get()::sendToServer);
if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer);
}
public void uploadResult(UploadResult result, @Nullable Component message) {

View File

@@ -4,7 +4,7 @@
package dan200.computercraft.client.gui;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
@@ -29,51 +29,51 @@ public final class ClientInputHandler implements InputHandler {
@Override
public void turnOn() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
}
@Override
public void shutdown() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
}
@Override
public void reboot() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
ClientPlatformHelper.get().sendToServer(new QueueEventServerMessage(menu, event, arguments));
ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
}
@Override
public void keyDown(int key, boolean repeat) {
ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.TYPE_REPEAT : KeyEventServerMessage.TYPE_DOWN, key));
}
@Override
public void keyUp(int key) {
ClientPlatformHelper.get().sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.TYPE_UP, key));
}
@Override
public void mouseClick(int button, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_CLICK, button, x, y));
}
@Override
public void mouseUp(int button, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_UP, button, x, y));
}
@Override
public void mouseDrag(int button, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_DRAG, button, x, y));
}
@Override
public void mouseScroll(int direction, int x, int y) {
ClientPlatformHelper.get().sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.TYPE_SCROLL, direction, x, y));
}
}

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.network;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import net.minecraft.client.Minecraft;
/**
* Methods for sending packets from clients to the server.
*/
public final class ClientNetworking {
private ClientNetworking() {
}
/**
* Send a network message to the server.
*
* @param message The message to send.
*/
public static void sendToServer(NetworkMessage<ServerNetworkContext> message) {
var connection = Minecraft.getInstance().getConnection();
if (connection != null) connection.send(ClientPlatformHelper.get().createPacket(message));
}
}

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import dan200.computercraft.client.ClientTableFormatter;
import dan200.computercraft.client.gui.AbstractComputerScreen;
import dan200.computercraft.client.gui.OptionScreen;
@@ -17,30 +18,30 @@ import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* The base implementation of {@link ClientNetworkContext}.
* <p>
* This should be extended by mod loader specific modules with the remaining abstract methods.
* The client-side implementation of {@link ClientNetworkContext}.
*/
public abstract class AbstractClientNetworkContext implements ClientNetworkContext {
@AutoService(ClientNetworkContext.class)
public final class ClientNetworkContextImpl implements ClientNetworkContext {
@Override
public final void handleChatTable(TableBuilder table) {
public void handleChatTable(TableBuilder table) {
ClientTableFormatter.INSTANCE.display(table);
}
@Override
public final void handleComputerTerminal(int containerId, TerminalState terminal) {
public void handleComputerTerminal(int containerId, TerminalState terminal) {
Player player = Minecraft.getInstance().player;
if (player != null && player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
menu.updateTerminal(terminal);
@@ -48,7 +49,7 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
}
@Override
public final void handleMonitorData(BlockPos pos, TerminalState terminal) {
public void handleMonitorData(BlockPos pos, TerminalState terminal) {
var player = Minecraft.getInstance().player;
if (player == null) return;
@@ -59,44 +60,46 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
}
@Override
public final void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name) {
var mc = Minecraft.getInstance();
ClientPlatformHelper.get().playStreamingMusic(pos, sound);
if (name != null) mc.gui.setNowPlaying(Component.literal(name));
}
@Override
public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
var computer = ClientPocketComputers.get(instanceId, terminal.colour);
computer.setState(state, lightState);
if (terminal.hasTerminal()) computer.setTerminal(terminal);
}
@Override
public final void handlePocketComputerDeleted(int instanceId) {
public void handlePocketComputerDeleted(int instanceId) {
ClientPocketComputers.remove(instanceId);
}
@Override
public final void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume);
public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
}
@Override
public final void handleSpeakerAudioPush(UUID source, ByteBuf buffer) {
SpeakerManager.getSound(source).pushAudio(buffer);
}
@Override
public final void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
public void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
SpeakerManager.moveSound(source, reifyPosition(position));
}
@Override
public final void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
public void handleSpeakerPlay(UUID source, SpeakerPosition.Message position, ResourceLocation sound, float volume, float pitch) {
SpeakerManager.getSound(source).playSound(reifyPosition(position), sound, volume, pitch);
}
@Override
public final void handleSpeakerStop(UUID source) {
public void handleSpeakerStop(UUID source) {
SpeakerManager.stopSound(source);
}
@Override
public final void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
public void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage) {
var minecraft = Minecraft.getInstance();
var screen = OptionScreen.unwrap(minecraft.screen);

View File

@@ -9,6 +9,10 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerGamePacketListener;
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
@@ -18,11 +22,12 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
}
/**
* Send a network message to the server.
* Convert a serverbound {@link NetworkMessage} to a Minecraft {@link Packet}.
*
* @param message The message to send.
* @param message The messsge to convert.
* @return The converted message.
*/
void sendToServer(NetworkMessage<ServerNetworkContext> message);
Packet<ServerGamePacketListener> createPacket(NetworkMessage<ServerNetworkContext> message);
/**
* Render a {@link BakedModel}, using any loader-specific hooks.
@@ -35,4 +40,13 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
* @param tints Block colour tints to apply to the model.
*/
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints);
/**
* Play a record at a particular position.
*
* @param pos The position to play this record.
* @param sound The record to play, or {@code null} to stop it.
* @see net.minecraft.client.renderer.LevelRenderer#playStreamingMusic(SoundEvent, BlockPos)
*/
void playStreamingMusic(BlockPos pos, @Nullable SoundEvent sound);
}

View File

@@ -6,7 +6,7 @@ package dan200.computercraft.client.sound;
import com.mojang.blaze3d.audio.Channel;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import io.netty.buffer.ByteBuf;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.sounds.AudioStream;
import net.minecraft.client.sounds.SoundEngine;
import org.lwjgl.BufferUtils;
@@ -36,7 +36,7 @@ class DfpwmStream implements AudioStream {
/**
* The {@link Channel} which this sound is playing on.
*
* @see SpeakerInstance#pushAudio(ByteBuf)
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
*/
@Nullable
Channel channel;
@@ -44,7 +44,7 @@ class DfpwmStream implements AudioStream {
/**
* The underlying {@link SoundEngine} executor.
*
* @see SpeakerInstance#pushAudio(ByteBuf)
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
* @see SoundEngine#executor
*/
@Nullable
@@ -58,12 +58,12 @@ class DfpwmStream implements AudioStream {
DfpwmStream() {
}
void push(ByteBuf input) {
var readable = input.readableBytes();
void push(ByteBuffer input) {
var readable = input.remaining();
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
for (var i = 0; i < readable; i++) {
var inputByte = input.readByte();
var inputByte = input.get();
for (var j = 0; j < 8; j++) {
var currentBit = (inputByte & 1) != 0;
var target = currentBit ? 127 : -128;

View File

@@ -7,11 +7,11 @@ package dan200.computercraft.client.sound;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
@@ -25,7 +25,7 @@ public class SpeakerInstance {
SpeakerInstance() {
}
public synchronized void pushAudio(ByteBuf buffer) {
private void pushAudio(ByteBuffer buffer) {
var sound = this.sound;
var stream = currentStream;
@@ -43,7 +43,9 @@ public class SpeakerInstance {
}
}
public void playAudio(SpeakerPosition position, float volume) {
public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) {
pushAudio(buffer);
var soundManager = Minecraft.getInstance().getSoundManager();
if (sound != null && sound.stream != currentStream) {

View File

@@ -11,11 +11,14 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.impl.PlatformHelper;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager;
import net.minecraft.client.Minecraft;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.WeakHashMap;
@@ -24,14 +27,15 @@ import java.util.stream.Stream;
/**
* A registry of {@link TurtleUpgradeModeller}s.
*
* @see dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller)
*/
public final class TurtleUpgradeModellers {
private static final Logger LOG = LoggerFactory.getLogger(TurtleUpgradeModellers.class);
private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side) ->
new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
private static final Map<TurtleUpgradeSerialiser<?>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
private static volatile boolean fetchedModels;
/**
* In order to avoid a double lookup of {@link ITurtleUpgrade} to {@link UpgradeManager.UpgradeWrapper} to
@@ -45,12 +49,18 @@ public final class TurtleUpgradeModellers {
}
public static <T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
synchronized (turtleModels) {
if (turtleModels.containsKey(serialiser)) {
throw new IllegalStateException("Modeller already registered for serialiser");
}
if (fetchedModels) {
// TODO(1.20.4): Replace with an error.
LOG.warn(
"Turtle upgrade serialiser {} was registered too late, its models may not be loaded correctly. If you are " +
"the mod author, you may be using a deprecated API - see https://github.com/cc-tweaked/CC-Tweaked/pull/1684 " +
"for further information.",
PlatformHelper.get().getRegistryKey(TurtleUpgradeSerialiser.registryId(), serialiser)
);
}
turtleModels.put(serialiser, modeller);
if (turtleModels.putIfAbsent(serialiser, modeller) != null) {
throw new IllegalStateException("Modeller already registered for serialiser");
}
}
@@ -75,6 +85,7 @@ public final class TurtleUpgradeModellers {
}
public static Stream<ResourceLocation> getDependencies() {
fetchedModels = true;
return turtleModels.values().stream().flatMap(x -> x.getDependencies().stream());
}
}

View File

@@ -1,13 +0,0 @@
{
"required": true,
"package": "dan200.computercraft.mixin.client",
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"injectors": {
"defaultRequire": 1
},
"client": [
"ClientPacketListenerMixin"
],
"refmap": "client-computercraft.refmap.json"
}

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
/**
* Wraps an existing {@link DataProvider}, passing generated JSON through {@link PrettyJsonWriter}.
*
* @param provider The provider to wrap.
* @param <T> The type of the provider to wrap.
*/
public record PrettyDataProvider<T extends DataProvider>(T provider) implements DataProvider {
@Override
public CompletableFuture<?> run(CachedOutput cachedOutput) {
return provider.run(new Output(cachedOutput));
}
@Override
public String getName() {
return provider.getName();
}
private record Output(CachedOutput output) implements CachedOutput {
@SuppressWarnings("deprecation")
private static final HashFunction HASH_FUNCTION = Hashing.sha1();
@Override
public void writeIfNeeded(Path path, byte[] bytes, HashCode hashCode) throws IOException {
if (path.getFileName().toString().endsWith(".json")) {
bytes = PrettyJsonWriter.reformat(bytes);
hashCode = HASH_FUNCTION.hashBytes(bytes);
}
output.writeIfNeeded(path, bytes, hashCode);
}
}
}

View File

@@ -22,11 +22,10 @@ import java.util.List;
/**
* Alternative version of {@link JsonWriter} which attempts to lay out the JSON in a more compact format.
* <p>
* Yes, this is at least a little deranged.
*
* @see PrettyDataProvider
*/
public class PrettyJsonWriter extends JsonWriter {
public static final boolean ENABLED = System.getProperty("cct.pretty-json") != null;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
private static final int MAX_WIDTH = 120;
@@ -44,17 +43,6 @@ public class PrettyJsonWriter extends JsonWriter {
this.out = out;
}
/**
* Create a JSON writer. This will either be a pretty or normal version, depending on whether the global flag is
* set.
*
* @param out The writer to emit to.
* @return The constructed JSON writer.
*/
public static JsonWriter createWriter(Writer out) {
return ENABLED ? new PrettyJsonWriter(out) : new JsonWriter(out);
}
/**
* Reformat a JSON string with our pretty printer.
*
@@ -62,8 +50,6 @@ public class PrettyJsonWriter extends JsonWriter {
* @return The reformatted string.
*/
public static byte[] reformat(byte[] contents) {
if (!ENABLED) return contents;
JsonElement object;
try (var reader = new InputStreamReader(new ByteArrayInputStream(contents), StandardCharsets.UTF_8)) {
object = GSON.fromJson(reader, JsonElement.class);

View File

@@ -123,7 +123,7 @@ public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends
// TODO: Can we track which mod this resource came from and use that instead? It's theoretically possible,
// but maybe not ideal for datapacks.
var modId = id.getNamespace();
if (modId.equals("minecraft") || modId.equals("")) modId = ComputerCraftAPI.MOD_ID;
if (modId.equals("minecraft") || modId.isEmpty()) modId = ComputerCraftAPI.MOD_ID;
var upgrade = serialiser.fromJson(id, root);
if (!upgrade.getUpgradeID().equals(id)) {

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin;
import dan200.computercraft.data.PrettyJsonWriter;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(targets = "net/minecraft/data/HashCache$CacheUpdater")
public class CacheUpdaterMixin {
@SuppressWarnings("UnusedMethod")
@ModifyArg(
method = "writeIfNeeded",
at = @At(value = "INVOKE", target = "Ljava/nio/file/Files;write(Ljava/nio/file/Path;[B[Ljava/nio/file/OpenOption;)Ljava/nio/file/Path;"),
require = 0
)
private byte[] reformatJson(byte[] contents) {
// It would be cleaner to do this inside DataProvider.saveStable, but Forge's version of Mixin doesn't allow us
// to inject into interfaces.
return PrettyJsonWriter.reformat(contents);
}
}

View File

@@ -54,7 +54,12 @@ import static net.minecraft.commands.Commands.literal;
public final class CommandComputerCraft {
public static final UUID SYSTEM_UUID = new UUID(0, 0);
public static final String OPEN_COMPUTER = "computercraft open-computer ";
/**
* The client-side command to open the folder. Ideally this would live under the main {@code computercraft}
* namespace, but unfortunately that overrides commands, rather than merging them.
*/
public static final String CLIENT_OPEN_FOLDER = "computercraft-computer-folder";
private CommandComputerCraft() {
}
@@ -389,7 +394,7 @@ public final class CommandComputerCraft {
return link(
text("\u270E"),
"/" + OPEN_COMPUTER + id,
"/" + CLIENT_OPEN_FOLDER + " " + id,
Component.translatable("commands.computercraft.dump.open_path")
);
}

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.shared.command.text;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.command.CommandUtils;
import dan200.computercraft.shared.network.client.ChatTableClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
@@ -105,7 +105,7 @@ public class TableBuilder {
if (CommandUtils.isPlayer(source)) {
trim(18);
var player = (ServerPlayer) Nullability.assertNonNull(source.getEntity());
PlatformHelper.get().sendToPlayer(new ChatTableClientMessage(this), player);
ServerNetworking.sendToPlayer(new ChatTableClientMessage(this), player);
} else {
trim(100);
new ServerTableFormatter(source).display(this);

View File

@@ -25,7 +25,6 @@ import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -51,7 +50,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
private boolean fresh = false;
private int invalidSides = 0;
private final ComponentAccess<IPeripheral> peripherals = PlatformHelper.get().createPeripheralAccess(d -> invalidSides |= 1 << d.ordinal());
private final ComponentAccess<IPeripheral> peripherals = PlatformHelper.get().createPeripheralAccess(this, d -> invalidSides |= 1 << d.ordinal());
private LockCode lockCode = LockCode.NO_LOCK;
@@ -218,7 +217,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
var localDir = remapToLocalSide(dir);
if (isPeripheralBlockedOnSide(localDir)) return;
var peripheral = peripherals.get((ServerLevel) getLevel(), getBlockPos(), dir);
var peripheral = peripherals.get(dir);
computer.setPeripheral(localDir, peripheral);
}

View File

@@ -20,28 +20,24 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
public class CommandComputerBlockEntity extends ComputerBlockEntity {
public class CommandReceiver implements CommandSource {
private final Map<Integer, String> output = new HashMap<>();
private final List<String> output = new ArrayList<>();
public void clearOutput() {
output.clear();
}
public Map<Integer, String> getOutput() {
return output;
}
public Map<Integer, String> copyOutput() {
return new HashMap<>(output);
public List<String> copyOutput() {
return new ArrayList<>(output);
}
@Override
public void sendSystemMessage(Component textComponent) {
output.put(output.size() + 1, textComponent.getString());
output.add(textComponent.getString());
}
@Override

View File

@@ -20,7 +20,7 @@ import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -142,7 +142,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
for (var player : server.getPlayerList().getPlayers()) {
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
PlatformHelper.get().sendToPlayer(createPacket.apply(player.containerMenu), player);
ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
}
}
}

View File

@@ -11,7 +11,7 @@ import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.UploadResultMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.chat.Component;
@@ -138,7 +138,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
return;
}
PlatformHelper.get().sendToPlayer(finishUpload(uploader), uploader);
ServerNetworking.sendToPlayer(finishUpload(uploader), uploader);
}
private UploadResultMessage finishUpload(ServerPlayer player) {
@@ -159,7 +159,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
toUpload.stream().map(x -> new TransferredFile(x.getName(), new ByteBufferChannel(x.getBytes()))).toList(),
() -> {
if (player.isAlive() && player.containerMenu == owner) {
PlatformHelper.get().sendToPlayer(UploadResultMessage.consumed(owner), player);
ServerNetworking.sendToPlayer(UploadResultMessage.consumed(owner), player);
}
}),
});

View File

@@ -224,7 +224,7 @@ public final class ConfigSpec {
.defineInRange("max_requests", CoreConfig.httpMaxRequests, 0, Integer.MAX_VALUE);
httpMaxWebsockets = builder
.comment("The number of websockets a computer can have open at one time. Set to 0 for unlimited.")
.comment("The number of websockets a computer can have open at one time.")
.defineInRange("max_websockets", CoreConfig.httpMaxWebsockets, 1, Integer.MAX_VALUE);
builder

View File

@@ -4,30 +4,24 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* The context under which clientbound packets are evaluated.
*/
public interface ClientNetworkContext {
static ClientNetworkContext get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ClientNetworkContext.class, Instance.ERROR) : instance;
}
void handleChatTable(TableBuilder table);
void handleComputerTerminal(int containerId, TerminalState terminal);
@@ -40,9 +34,7 @@ public interface ClientNetworkContext {
void handlePocketComputerDeleted(int instanceId);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume);
void handleSpeakerAudioPush(UUID source, ByteBuf buffer);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
@@ -51,18 +43,4 @@ public interface ClientNetworkContext {
void handleSpeakerStop(UUID source);
void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage);
final class Instance {
static final @Nullable ClientNetworkContext INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(ClientNetworkContext.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
}

View File

@@ -11,12 +11,9 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
/**
* Starts a sound on the client.
* <p>
@@ -27,7 +24,7 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
private final UUID source;
private final SpeakerPosition.Message pos;
private final @Nullable ByteBuffer content;
private final ByteBuffer content;
private final float volume;
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
@@ -42,10 +39,9 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
pos = SpeakerPosition.Message.read(buf);
volume = buf.readFloat();
// TODO: Remove this, so we no longer need a getter for ClientNetworkContext. However, doing so without
// leaking or redundantly copying the buffer is hard.
ClientNetworkContext.get().handleSpeakerAudioPush(source, buf);
content = null;
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
content = ByteBuffer.wrap(bytes);
}
@Override
@@ -53,12 +49,12 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
buf.writeUUID(source);
pos.write(buf);
buf.writeFloat(volume);
buf.writeBytes(assertNonNull(content).duplicate());
buf.writeBytes(content.duplicate());
}
@Override
public void handle(ClientNetworkContext context) {
context.handleSpeakerAudio(source, pos, volume);
context.handleSpeakerAudio(source, pos, volume, content);
}
@Override

View File

@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.Vec3;
import java.util.Collection;
/**
* Methods for sending network messages from the server to clients.
*/
public final class ServerNetworking {
private ServerNetworking() {
}
/**
* Send a message to a specific player.
*
* @param message The message to send.
* @param player The player to send it to.
*/
public static void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player) {
player.connection.send(PlatformHelper.get().createPacket(message));
}
/**
* Send a message to a set of players.
*
* @param message The message to send.
* @param players The players to send it to.
*/
public static void sendToPlayers(NetworkMessage<ClientNetworkContext> message, Collection<ServerPlayer> players) {
if (players.isEmpty()) return;
var packet = PlatformHelper.get().createPacket(message);
for (var player : players) player.connection.send(packet);
}
/**
* Send a message to all players.
*
* @param message The message to send.
* @param server The current server.
*/
public static void sendToAllPlayers(NetworkMessage<ClientNetworkContext> message, MinecraftServer server) {
server.getPlayerList().broadcastAll(PlatformHelper.get().createPacket(message));
}
/**
* Send a message to all players around a point.
*
* @param message The message to send.
* @param level The level the point is in.
* @param pos The centre position.
* @param distance The distance to the centre players must be within.
*/
public static void sendToAllAround(NetworkMessage<ClientNetworkContext> message, ServerLevel level, Vec3 pos, float distance) {
level.getServer().getPlayerList().broadcast(null, pos.x, pos.y, pos.z, distance, level.dimension(), PlatformHelper.get().createPacket(message));
}
/**
* Send a message to all players tracking a chunk.
*
* @param message The message to send.
* @param chunk The chunk players must be tracking.
*/
public static void sendToAllTracking(NetworkMessage<ClientNetworkContext> message, LevelChunk chunk) {
var packet = PlatformHelper.get().createPacket(message);
for (var player : ((ServerChunkCache) chunk.getLevel().getChunkSource()).chunkMap.getPlayers(chunk.getPos(), false)) {
player.connection.send(packet);
}
}
}

View File

@@ -7,11 +7,12 @@ package dan200.computercraft.shared.peripheral.diskdrive;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.common.AbstractContainerBlockEntity;
import dan200.computercraft.shared.network.client.PlayRecordClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -32,6 +33,28 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* The underlying block entity for disk drives. This holds the main logic for the {@linkplain DiskDrivePeripheral disk
* drive peripheral}, such as handling mounts and {@linkplain DiskDrivePeripheral#playAudio() playing audio}.
* <p>
* Most disk drive peripheral methods execute on the computer thread (largely due to historic reasons). This causes some
* problems, as the disk item could be read by both the computer thread (via peripheral calls) and main thread (via
* Minecraft inventory interaction).
* <p>
* To solve this, we use an immutable {@link MediaStack}, which holds an immutable version of the current
* {@link ItemStack} (and its corresponding {@link IMedia}). When the {@linkplain #setChanged() inventory is changed},
* we {@linkplain #updateMedia() update the media stack} and recompute mounts.
* <p>
* This is somewhat complicated by {@link #attach(IComputerAccess)}. As that can happen on the computer thread and
* may mutate the stack (when {@link IMedia#createDataMount(ItemStack, ServerLevel)} assigns an ID for the first time),
* we need a way to safely update the inventory. To solve this, all internal non-inventory interactions with disk drives
* treat the media stack as the "primary" stack. This allows us to atomically update it, and then sync it back to the
* main inventory ({@link #updateMediaStack(ItemStack, boolean)}) either directly ({@link #updateDiskFromMedia()}) or
* on the next block tick ({@link #stackDirty}). This does mean there's a one-tick delay where the inventory may be
* out-of-date, but that should happen very rarely.
*
* @see DiskDrivePeripheral
*/
public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
private static final String NBT_ITEM = "Item";
@@ -42,11 +65,13 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
private final DiskDrivePeripheral peripheral = new DiskDrivePeripheral(this);
private final @GuardedBy("this") Map<IComputerAccess, MountInfo> computers = new HashMap<>();
private final NonNullList<ItemStack> inventory = NonNullList.withSize(1, ItemStack.EMPTY);
@GuardedBy("this")
private final Map<IComputerAccess, MountInfo> computers = new HashMap<>();
@GuardedBy("this")
private MediaStack media = MediaStack.EMPTY;
@GuardedBy("this")
private @Nullable Mount mount;
private boolean recordPlaying = false;
@@ -54,7 +79,12 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
// then read them when ticking.
private final AtomicReference<RecordCommand> recordQueued = new AtomicReference<>(null);
private final AtomicBoolean ejectQueued = new AtomicBoolean(false);
private final AtomicBoolean mountQueued = new AtomicBoolean(false);
/**
* Whether the stack in {@link #media} has been modified on the computer thread, and needs to be written back to the
* inventory on the main thread.
*/
private final AtomicBoolean stackDirty = new AtomicBoolean(false);
public DiskDriveBlockEntity(BlockEntityType<DiskDriveBlockEntity> type, BlockPos pos, BlockState state) {
super(type, pos, state);
@@ -66,7 +96,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
@Override
public void clearRemoved() {
updateItem();
updateMedia();
}
@Override
@@ -93,12 +123,14 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
}
void serverTick() {
if (stackDirty.getAndSet(false)) updateDiskFromMedia();
if (ejectQueued.getAndSet(false)) ejectContents();
var recordQueued = this.recordQueued.getAndSet(null);
if (recordQueued != null) {
switch (recordQueued) {
case PLAY -> {
var media = getMedia();
var record = media.getAudio();
if (record != null) {
recordPlaying = true;
@@ -112,12 +144,6 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
}
}
}
if (mountQueued.get()) {
synchronized (this) {
mountAll();
}
}
}
@Override
@@ -127,38 +153,46 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
@Override
public void setChanged() {
if (level != null && !level.isClientSide) updateItem();
if (level != null && !level.isClientSide) updateMedia();
super.setChanged();
}
private void updateItem() {
var newDisk = getDiskStack();
if (ItemStack.isSameItemSameTags(newDisk, media.stack)) return;
/**
* Called on the server after the item has changed. This unmounts the old media and mounts the new one.
*/
private synchronized void updateMedia() {
var newStack = getDiskStack();
if (ItemStack.isSameItemSameTags(newStack, media.stack())) return;
var media = MediaStack.of(newDisk);
var newMedia = MediaStack.of(newStack);
if (newDisk.isEmpty()) {
if (newStack.isEmpty()) {
updateBlockState(DiskDriveState.EMPTY);
} else {
updateBlockState(media.media != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
updateBlockState(newMedia.media() != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
}
synchronized (this) {
// Unmount old disk
if (!this.media.stack.isEmpty()) {
for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
// Unmount old disk
if (!media.stack().isEmpty()) {
for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
}
// Stop music
if (recordPlaying) {
stopRecord();
recordPlaying = false;
}
// Use our new media, and (if needed) mount the new disk.
mount = null;
media = newMedia;
stackDirty.set(false);
if (!newStack.isEmpty() && !computers.isEmpty()) {
var mount = getOrCreateMount(true);
for (var entry : computers.entrySet()) {
mountDisk(entry.getKey(), entry.getValue(), mount);
}
// Stop music
if (recordPlaying) {
stopRecord();
recordPlaying = false;
}
mount = null;
this.media = media;
mountAll();
}
}
@@ -166,7 +200,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
return getItem(0);
}
MediaStack getMedia() {
synchronized MediaStack getMedia() {
return media;
}
@@ -181,16 +215,31 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
}
/**
* Update the current disk stack, assuming the underlying item does not change. Unlike
* {@link #setDiskStack(ItemStack)} this will not change any mounts.
*
* @param stack The new disk stack.
* Update the inventory's disk stack from the media stack. Unlike {@link #setDiskStack(ItemStack)} this will not
* change any mounts.
*/
void updateDiskStack(ItemStack stack) {
setItem(0, stack);
if (!ItemStack.isSameItemSameTags(stack, media.stack)) {
media = MediaStack.of(stack);
super.setChanged();
private synchronized void updateDiskFromMedia() {
// Write back the item to the main inventory, and then mark it as dirty.
setItem(0, media.stack().copy());
super.setChanged();
}
/**
* Atomically update {@link #media}'s stack, then sync it back to the main inventory.
*
* @param stack The original stack.
* @param immediate Whether to do this immediately (when called from the main thread) or asynchronously (when called
* from the computer thread).
*/
@GuardedBy("this")
private void updateMediaStack(ItemStack stack, boolean immediate) {
if (ItemStack.isSameItemSameTags(media.stack(), stack)) return;
media = new MediaStack(stack, media.media());
if (immediate) {
updateDiskFromMedia();
} else {
stackDirty.set(true);
}
}
@@ -212,7 +261,9 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
synchronized (this) {
var info = new MountInfo();
computers.put(computer, info);
mountQueued.set(true);
if (!media.stack().isEmpty()) {
mountDisk(computer, info, getOrCreateMount(level instanceof ServerLevel l && l.getServer().isSameThread()));
}
}
}
@@ -234,53 +285,50 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
ejectQueued.set(true);
}
/**
* Add our mount to all computers.
*/
@GuardedBy("this")
private void mountAll() {
doMountAll();
mountQueued.set(false);
synchronized MountResult setDiskLabel(@Nullable String label) {
if (media.media() == null) return MountResult.NO_MEDIA;
// Set the label, and write it back to the media stack.
var stack = media.stack().copy();
if (!media.media().setLabel(stack, label)) return MountResult.NOT_ALLOWED;
updateMediaStack(stack, true);
return MountResult.CHANGED;
}
/**
* The worker for {@link #mountAll()}. This is responsible for creating the mount and placing it on all computers.
*/
@GuardedBy("this")
private void doMountAll() {
if (computers.isEmpty() || media.media == null) return;
private @Nullable Mount getOrCreateMount(boolean immediate) {
if (media.media() == null) return null;
if (mount != null) return mount;
if (mount == null) {
var stack = getDiskStack();
mount = media.media.createDataMount(stack, (ServerLevel) level);
setDiskStack(stack);
}
// Set the id (if needed) and write it back to the media stack.
var stack = media.stack().copy();
mount = media.media().createDataMount(stack, (ServerLevel) level);
updateMediaStack(stack, immediate);
if (mount == null) return;
return mount;
}
for (var entry : computers.entrySet()) {
var computer = entry.getKey();
var info = entry.getValue();
if (info.mountPath != null) continue;
if (mount instanceof WritableMount writable) {
// Try mounting at the lowest numbered "disk" name we can
var n = 1;
while (info.mountPath == null) {
info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
n++;
}
} else {
// Try mounting at the lowest numbered "disk" name we can
var n = 1;
while (info.mountPath == null) {
info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount);
n++;
}
private static void mountDisk(IComputerAccess computer, MountInfo info, @Nullable Mount mount) {
if (mount instanceof WritableMount writable) {
// Try mounting at the lowest numbered "disk" name we can
var n = 1;
while (info.mountPath == null) {
info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
n++;
}
computer.queueEvent("disk", computer.getAttachmentName());
} else if (mount != null) {
// Try mounting at the lowest numbered "disk" name we can
var n = 1;
while (info.mountPath == null) {
info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount);
n++;
}
} else {
assert info.mountPath == null : "Mount path should be null";
}
computer.queueEvent("disk", computer.getAttachmentName());
}
private static void unmountDisk(IComputerAccess computer, MountInfo info) {
@@ -315,7 +363,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
}
private void sendMessage(PlayRecordClientMessage message) {
PlatformHelper.get().sendToAllAround(message, (ServerLevel) getLevel(), Vec3.atCenterOf(getBlockPos()), 64);
ServerNetworking.sendToAllAround(message, (ServerLevel) getLevel(), Vec3.atCenterOf(getBlockPos()), 64);
}
@Override
@@ -327,4 +375,10 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
PLAY,
STOP,
}
enum MountResult {
NO_MEDIA,
NOT_ALLOWED,
CHANGED,
}
}

View File

@@ -51,7 +51,7 @@ public class DiskDrivePeripheral implements IPeripheral {
*/
@LuaFunction
public final boolean isDiskPresent() {
return !diskDrive.getMedia().stack.isEmpty();
return !diskDrive.getMedia().stack().isEmpty();
}
/**
@@ -64,7 +64,7 @@ public class DiskDrivePeripheral implements IPeripheral {
@LuaFunction
public final Object[] getDiskLabel() {
var media = diskDrive.getMedia();
return media.media == null ? null : new Object[]{ media.media.getLabel(media.stack) };
return media.media() == null ? null : new Object[]{ media.media().getLabel(media.stack()) };
}
/**
@@ -80,15 +80,11 @@ public class DiskDrivePeripheral implements IPeripheral {
*/
@LuaFunction(mainThread = true)
public final void setDiskLabel(Optional<String> label) throws LuaException {
var media = diskDrive.getMedia();
if (media.media == null) return;
// We're on the main thread so the stack and media should be in sync.
var stack = diskDrive.getDiskStack();
if (!media.media.setLabel(stack, label.map(StringUtil::normaliseLabel).orElse(null))) {
throw new LuaException("Disk label cannot be changed");
switch (diskDrive.setDiskLabel(label.map(StringUtil::normaliseLabel).orElse(null))) {
case NOT_ALLOWED -> throw new LuaException("Disk label cannot be changed");
case CHANGED, NO_MEDIA -> {
}
}
diskDrive.updateDiskStack(stack);
}
/**
@@ -172,7 +168,7 @@ public class DiskDrivePeripheral implements IPeripheral {
@Nullable
@LuaFunction
public final Object[] getDiskID() {
var disk = diskDrive.getMedia().stack;
var disk = diskDrive.getMedia().stack();
return disk.getItem() instanceof DiskItem ? new Object[]{ DiskItem.getDiskID(disk) } : null;
}

View File

@@ -13,19 +13,14 @@ import javax.annotation.Nullable;
/**
* An immutable snapshot of the current disk. This allows us to read the stack in a thread-safe manner.
*
* @param stack An immutable {@link ItemStack}.
* @param media The associated {@link IMedia} instance for this stack.
*/
final class MediaStack {
record MediaStack(ItemStack stack, @Nullable IMedia media) {
static final MediaStack EMPTY = new MediaStack(ItemStack.EMPTY, null);
final ItemStack stack;
final @Nullable IMedia media;
private MediaStack(ItemStack stack, @Nullable IMedia media) {
this.stack = stack;
this.media = media;
}
public static MediaStack of(ItemStack stack) {
static MediaStack of(ItemStack stack) {
if (stack.isEmpty()) return EMPTY;
var freshStack = stack.copy();

View File

@@ -6,7 +6,7 @@ package dan200.computercraft.shared.peripheral.generic;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
@@ -32,5 +32,5 @@ public interface ComponentLookup<C extends Runnable> {
* @return The found component, or {@code null} if not present.
*/
@Nullable
Object find(Level level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, C invalidate);
Object find(ServerLevel level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side, C invalidate);
}

View File

@@ -11,7 +11,7 @@ import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,7 +43,7 @@ public final class GenericPeripheralProvider<C extends Runnable> {
if (!lookups.contains(lookup)) lookups.add(lookup);
}
public void forEachMethod(MethodSupplier<PeripheralMethod> methods, Level level, BlockPos pos, Direction side, BlockEntity blockEntity, C invalidate, MethodSupplier.TargetedConsumer<PeripheralMethod> consumer) {
public void forEachMethod(MethodSupplier<PeripheralMethod> methods, ServerLevel level, BlockPos pos, Direction side, BlockEntity blockEntity, C invalidate, MethodSupplier.TargetedConsumer<PeripheralMethod> consumer) {
methods.forEachMethod(blockEntity, consumer);
for (var lookup : lookups) {
@@ -53,17 +53,11 @@ public final class GenericPeripheralProvider<C extends Runnable> {
}
@Nullable
public IPeripheral getPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, C invalidate) {
public IPeripheral getPeripheral(ServerLevel level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, C invalidate) {
if (blockEntity == null) return null;
var server = level.getServer();
if (server == null) {
LOG.warn("Fetching peripherals on a non-server level {}.", level, new IllegalStateException("Fetching peripherals on a non-server level."));
return null;
}
var builder = new GenericPeripheralBuilder();
forEachMethod(ServerContext.get(server).peripheralMethods(), level, pos, side, blockEntity, invalidate, builder::addMethod);
forEachMethod(ServerContext.get(level.getServer()).peripheralMethods(), level, pos, side, blockEntity, invalidate, builder::addMethod);
return builder.toPeripheral(blockEntity, side);
}
}

View File

@@ -18,7 +18,6 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -60,10 +59,11 @@ public class CableBlockEntity extends BlockEntity {
private boolean invalidPeripheral;
private boolean peripheralAccessAllowed;
private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(this::queueRefreshPeripheral);
private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
private @Nullable Runnable modemChanged;
private boolean connectionsFormed = false;
private boolean connectionsChanged = false;
private final WiredModemElement cable = new CableElement();
private final WiredNode node = cable.getNode();
@@ -88,7 +88,7 @@ public class CableBlockEntity extends BlockEntity {
}
};
private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(x -> connectionsChanged());
private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(this, x -> scheduleConnectionsChanged());
public CableBlockEntity(BlockEntityType<? extends CableBlockEntity> type, BlockPos pos, BlockState state) {
super(type, pos, state);
@@ -237,10 +237,18 @@ public class CableBlockEntity extends BlockEntity {
updateConnectedPeripherals();
}
}
if (connectionsChanged) connectionsChanged();
}
private void scheduleConnectionsChanged() {
connectionsChanged = true;
TickScheduler.schedule(tickToken);
}
void connectionsChanged() {
if (getLevel().isClientSide) return;
connectionsChanged = false;
var state = getBlockState();
var world = getLevel();
@@ -249,7 +257,7 @@ public class CableBlockEntity extends BlockEntity {
var offset = current.relative(facing);
if (!world.isLoaded(offset)) continue;
var element = connectedElements.get((ServerLevel) world, current, facing);
var element = connectedElements.get(facing);
if (element == null) continue;
var node = element.getNode();

View File

@@ -17,7 +17,6 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
@@ -75,21 +74,22 @@ public class WiredModemFullBlockEntity extends BlockEntity {
private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
private boolean connectionsFormed = false;
private boolean connectionsChanged = false;
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken));
private final WiredModemElement element = new FullElement(this);
private final WiredNode node = element.getNode();
private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(x -> connectionsChanged());
private final ComponentAccess<WiredElement> connectedElements = PlatformHelper.get().createWiredElementAccess(this, x -> scheduleConnectionsChanged());
private int invalidSides = 0;
public WiredModemFullBlockEntity(BlockEntityType<WiredModemFullBlockEntity> type, BlockPos pos, BlockState state) {
super(type, pos, state);
var peripheralAccess = PlatformHelper.get().createPeripheralAccess(this, this::queueRefreshPeripheral);
for (var i = 0; i < peripherals.length; i++) {
var facing = Direction.from3DDataValue(i);
peripherals[i] = new WiredModemLocalPeripheral(() -> queueRefreshPeripheral(facing));
peripherals[i] = new WiredModemLocalPeripheral(peripheralAccess);
}
}
@@ -205,10 +205,18 @@ public class WiredModemFullBlockEntity extends BlockEntity {
updateConnectedPeripherals();
}
}
if (connectionsChanged) connectionsChanged();
}
private void scheduleConnectionsChanged() {
connectionsChanged = true;
TickScheduler.schedule(tickToken);
}
private void connectionsChanged() {
if (getLevel().isClientSide) return;
connectionsChanged = false;
var world = getLevel();
var current = getBlockPos();
@@ -216,7 +224,7 @@ public class WiredModemFullBlockEntity extends BlockEntity {
var offset = current.relative(facing);
if (!world.isLoaded(offset)) continue;
var element = connectedElements.get((ServerLevel) getLevel(), getBlockPos(), facing);
var element = connectedElements.get(facing);
if (element == null) continue;
node.connectTo(element.getNode());

View File

@@ -8,12 +8,10 @@ import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.platform.ComponentAccess;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
@@ -38,8 +36,8 @@ public final class WiredModemLocalPeripheral {
private @Nullable IPeripheral peripheral;
private final ComponentAccess<IPeripheral> peripherals;
public WiredModemLocalPeripheral(Runnable invalidate) {
peripherals = PlatformHelper.get().createPeripheralAccess(x -> invalidate.run());
public WiredModemLocalPeripheral(ComponentAccess<IPeripheral> peripherals) {
this.peripherals = peripherals;
}
/**
@@ -126,7 +124,7 @@ public final class WiredModemLocalPeripheral {
if (world.getBlockState(offset).is(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE)) return null;
var peripheral = peripherals.get((ServerLevel) world, pos, direction);
var peripheral = peripherals.get(direction);
return peripheral instanceof WiredModemPeripheral ? null : peripheral;
}
}

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.shared.peripheral.monitor;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.client.MonitorClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.chunk.LevelChunk;
@@ -40,7 +40,7 @@ public final class MonitorWatcher {
if (serverMonitor == null || monitor.enqueued) continue;
var state = getState(monitor, serverMonitor);
PlatformHelper.get().sendToPlayer(new MonitorClientMessage(monitor.getBlockPos(), state), player);
ServerNetworking.sendToPlayer(new MonitorClientMessage(monitor.getBlockPos(), state), player);
}
}
@@ -66,7 +66,7 @@ public final class MonitorWatcher {
}
var state = getState(tile, monitor);
PlatformHelper.get().sendToAllTracking(new MonitorClientMessage(pos, state), chunk);
ServerNetworking.sendToAllTracking(new MonitorClientMessage(pos, state), chunk);
limit -= state.size();
}

View File

@@ -225,7 +225,9 @@ public final class PrinterBlockEntity extends AbstractContainerBlockEntity imple
var stack = PrintoutItem.createSingleFromTitleAndText(pageTitle, lines, colours);
for (var slot : BOTTOM_SLOTS) {
if (inventory.get(slot).isEmpty()) {
setItem(slot, stack);
inventory.set(slot, stack);
updateBlockState();
setChanged();
printing = false;
return true;
}

View File

@@ -15,14 +15,44 @@ import javax.annotation.Nullable;
import java.util.Optional;
/**
* The printer peripheral allows pages and books to be printed.
* The printer peripheral allows printing text onto pages. These pages can then be crafted together into printed pages
* or books.
* <p>
* ## Recipe
* Printers require ink (one of the coloured dyes) and paper in order to function. Once loaded, a new page can be
* started with {@link #newPage()}. Then the printer can be used similarly to a normal terminal; {@linkplain
* #write(Coerced) text can be written}, and {@linkplain #setCursorPos(int, int) the cursor moved}. Once all text has
* been printed, {@link #endPage()} should be called to finally print the page.
* <p>
* ## Recipes
* <div class="recipe-container">
* <mc-recipe recipe="computercraft:printer"></mc-recipe>
* <mc-recipe recipe="computercraft:printed_pages"></mc-recipe>
* <mc-recipe recipe="computercraft:printed_book"></mc-recipe>
* </div>
*
* @cc.usage Print a page titled "Hello" with a small message on it.
*
* <pre>{@code
* local printer = peripheral.find("printer")
*
* -- Start a new page, or print an error.
* if not printer.newPage() then
* error("Cannot start a new page. Do you have ink and paper?")
* end
*
* -- Write to the page
* printer.setPageTitle("Hello")
* printer.write("This is my first page")
* printer.setCursorPos(1, 3)
* printer.write("This is two lines below.")
*
* -- And finally print the page!
* if not printer.endPage() then
* error("Cannot end the page. Is there enough space?")
* end
* }</pre>
* @cc.module printer
* @cc.see cc.strings.wrap To wrap text before printing it.
*/
public class PrinterPeripheral implements IPeripheral {
private final PrinterBlockEntity printer;

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -32,7 +32,7 @@ public class SpeakerBlockEntity extends BlockEntity {
public void setRemoved() {
super.setRemoved();
if (level != null && !level.isClientSide) {
PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(peripheral.getSource()), Nullability.assertNonNull(getLevel().getServer()));
ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(peripheral.getSource()), Nullability.assertNonNull(getLevel().getServer()));
}
}

View File

@@ -17,7 +17,7 @@ import dan200.computercraft.shared.network.client.SpeakerAudioClientMessage;
import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage;
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.util.PauseAwareTimer;
import net.minecraft.ResourceLocationException;
import net.minecraft.core.BlockPos;
@@ -116,14 +116,14 @@ public abstract class SpeakerPeripheral implements IPeripheral {
// Stop the speaker and nuke the position, so we don't update it again.
if (shouldStop && lastPosition != null) {
lastPosition = null;
PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
return;
}
var now = PauseAwareTimer.getTime();
if (sound != null) {
lastPlayTime = clock;
PlatformHelper.get().sendToAllAround(
ServerNetworking.sendToAllAround(
new SpeakerPlayClientMessage(getSource(), position, sound.sound, sound.volume, sound.pitch),
(ServerLevel) level, pos, sound.volume * 16
);
@@ -131,7 +131,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
} else if (dfpwmState != null && dfpwmState.shouldSendPending(now)) {
// If clients need to receive another batch of audio, send it and then notify computers our internal buffer is
// free again.
PlatformHelper.get().sendToAllTracking(
ServerNetworking.sendToAllTracking(
new SpeakerAudioClientMessage(getSource(), position, dfpwmState.getVolume(), dfpwmState.pullPending(now)),
level.getChunkAt(BlockPos.containing(pos))
);
@@ -150,7 +150,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
// in the last second.
if (lastPosition != null && (clock - lastPositionTime) >= 20 && !lastPosition.withinDistance(position, 0.1)) {
// TODO: What to do when entities move away? How do we notify people left behind that they're gone.
PlatformHelper.get().sendToAllTracking(
ServerNetworking.sendToAllTracking(
new SpeakerMoveClientMessage(getSource(), position),
level.getChunkAt(BlockPos.containing(pos))
);

View File

@@ -6,7 +6,7 @@ package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
/**
@@ -25,6 +25,6 @@ public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral {
var server = level.getServer();
if (server == null || server.isStopped()) return;
PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
}
}

View File

@@ -4,9 +4,7 @@
package dan200.computercraft.shared.platform;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import javax.annotation.Nullable;
@@ -18,15 +16,11 @@ import javax.annotation.Nullable;
public interface ComponentAccess<T> {
/**
* Get a peripheral for the current block.
* <p>
* Both {@code level} and {@code pos} must be constant for the lifetime of the store.
*
* @param level The current level.
* @param pos The position of the block fetching the peripheral, for instance the computer or modem.
* @param direction The direction the peripheral is in.
* @return The peripheral, or {@literal null} if not found.
* @throws IllegalStateException If the level or position have changed.
*/
@Nullable
T get(ServerLevel level, BlockPos pos, Direction direction);
T get(Direction direction);
}

View File

@@ -20,9 +20,10 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
@@ -44,12 +45,10 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@@ -178,64 +177,32 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
<T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader);
/**
* Send a message to a specific player.
* Convert a clientbound {@link NetworkMessage} to a Minecraft {@link Packet}.
*
* @param message The message to send.
* @param player The player to send it to.
* @param message The messsge to convert.
* @return The converted message.
*/
void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player);
/**
* Send a message to a set of players.
*
* @param message The message to send.
* @param players The players to send it to.
*/
void sendToPlayers(NetworkMessage<ClientNetworkContext> message, Collection<ServerPlayer> players);
/**
* Send a message to all players.
*
* @param message The message to send.
* @param server The current server.
*/
void sendToAllPlayers(NetworkMessage<ClientNetworkContext> message, MinecraftServer server);
/**
* Send a message to all players around a point.
*
* @param message The message to send.
* @param level The level the point is in.
* @param pos The centre position.
* @param distance The distance to the centre players must be within.
*/
void sendToAllAround(NetworkMessage<ClientNetworkContext> message, ServerLevel level, Vec3 pos, float distance);
/**
* Send a message to all players tracking a chunk.
*
* @param message The message to send.
* @param chunk The chunk players must be tracking.
*/
void sendToAllTracking(NetworkMessage<ClientNetworkContext> message, LevelChunk chunk);
Packet<ClientGamePacketListener> createPacket(NetworkMessage<ClientNetworkContext> message);
/**
* Create a {@link ComponentAccess} for surrounding peripherals.
*
* @param owner The block entity requesting surrounding peripherals.
* @param invalidate The function to call when a neighbouring peripheral potentially changes. This <em>MAY NOT</em>
* include all changes, and so block updates should still be listened to.
* @return The peripheral component access.
*/
ComponentAccess<IPeripheral> createPeripheralAccess(Consumer<Direction> invalidate);
ComponentAccess<IPeripheral> createPeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate);
/**
* Create a {@link ComponentAccess} for surrounding wired nodes.
*
* @param owner The block entity requesting surrounding wired elements.
* @param invalidate The function to call when a neighbouring wired node potentially changes. This <em>MAY NOT</em>
* include all changes, and so block updates should still be listened to.
* @return The peripheral component access.
*/
ComponentAccess<WiredElement> createWiredElementAccess(Consumer<Direction> invalidate);
ComponentAccess<WiredElement> createWiredElementAccess(BlockEntity owner, Consumer<Direction> invalidate);
/**
* Determine if there is a wired element in the given direction. This is equivalent to

View File

@@ -15,7 +15,7 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
@@ -161,7 +161,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
if (sendState) {
// Broadcast the state to all players
tracking.addAll(getLevel().players());
PlatformHelper.get().sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
} else {
// Broadcast the state to new players.
List<ServerPlayer> added = new ArrayList<>();
@@ -169,7 +169,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
if (tracking.add(player)) added.add(player);
}
if (!added.isEmpty()) {
PlatformHelper.get().sendToPlayers(new PocketComputerDataMessage(this, false), added);
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added);
}
}
}
@@ -180,13 +180,13 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
if (entity instanceof ServerPlayer player && entity.isAlive()) {
// Broadcast the terminal to the current player.
PlatformHelper.get().sendToPlayer(new PocketComputerDataMessage(this, true), player);
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), player);
}
}
@Override
protected void onRemoved() {
super.onRemoved();
PlatformHelper.get().sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
}
}

View File

@@ -42,7 +42,6 @@ import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR;
import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED;
@@ -59,8 +58,6 @@ public class TurtleBrain implements TurtleAccessInternal {
private static final int ANIM_DURATION = 8;
public static final Predicate<Entity> PUSHABLE_ENTITY = entity -> !entity.isSpectator() && entity.getPistonPushReaction() != PushReaction.IGNORE;
private TurtleBlockEntity owner;
private @Nullable GameProfile owningPlayer;
@@ -694,7 +691,7 @@ public class TurtleBrain implements TurtleAccessInternal {
}
var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
var list = world.getEntitiesOfClass(Entity.class, aabb, PUSHABLE_ENTITY);
var list = world.getEntitiesOfClass(Entity.class, aabb, TurtleBrain::canPush);
if (!list.isEmpty()) {
double pushStep = 1.0f / ANIM_DURATION;
var pushStepX = moveDir.getStepX() * pushStep;
@@ -737,6 +734,10 @@ public class TurtleBrain implements TurtleAccessInternal {
}
}
private static boolean canPush(Entity entity) {
return !entity.isSpectator() && entity.getPistonPushReaction() != PushReaction.IGNORE;
}
private float getAnimationFraction(float f) {
var next = (float) animationProgress / ANIM_DURATION;
var previous = (float) lastAnimationProgress / ANIM_DURATION;

View File

@@ -19,9 +19,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
public final class NBTUtil {
private static final Logger LOG = LoggerFactory.getLogger(NBTUtil.class);
@@ -149,20 +147,20 @@ public final class NBTUtil {
}
case Tag.TAG_LIST: {
var list = (ListTag) tag;
Map<Integer, Object> map = new HashMap<>(list.size());
for (var i = 0; i < list.size(); i++) map.put(i, toLua(list.get(i)));
List<Object> map = new ArrayList<>(list.size());
for (var value : list) map.add(toLua(value));
return map;
}
case Tag.TAG_BYTE_ARRAY: {
var array = ((ByteArrayTag) tag).getAsByteArray();
Map<Integer, Byte> map = new HashMap<>(array.length);
for (var i = 0; i < array.length; i++) map.put(i + 1, array[i]);
List<Byte> map = new ArrayList<>(array.length);
for (var b : array) map.add(b);
return map;
}
case Tag.TAG_INT_ARRAY: {
var array = ((IntArrayTag) tag).getAsIntArray();
Map<Integer, Integer> map = new HashMap<>(array.length);
for (var i = 0; i < array.length; i++) map.put(i + 1, array[i]);
List<Integer> map = new ArrayList<>(array.length);
for (var j : array) map.add(j);
return map;
}

View File

@@ -27,12 +27,8 @@ import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import javax.annotation.Nullable;
import java.util.function.Predicate;
public final class WorldUtil {
@SuppressWarnings("UnnecessaryLambda")
private static final Predicate<Entity> CAN_COLLIDE = x -> x != null && x.isAlive() && x.isPickable();
public static boolean isLiquidBlock(Level world, BlockPos pos) {
if (!world.isInWorldBounds(pos)) return false;
return world.getBlockState(pos).liquid();
@@ -84,7 +80,7 @@ public final class WorldUtil {
Entity bestEntity = null;
Vec3 bestHit = null;
for (var entity : level.getEntities(source, bounds, WorldUtil.CAN_COLLIDE)) {
for (var entity : level.getEntities(source, bounds, WorldUtil::canCollide)) {
var aabb = entity.getBoundingBox().inflate(entity.getPickRadius());
// clip doesn't work when inside the entity. Just assume we've got a perfect match and break.
@@ -109,6 +105,10 @@ public final class WorldUtil {
return bestEntity == null ? null : new EntityHitResult(bestEntity, bestHit);
}
private static boolean canCollide(Entity entity) {
return entity != null && entity.isAlive() && entity.isPickable();
}
public static Vec3 getRayStart(Player entity) {
return entity.getEyePosition();
}

View File

@@ -1,13 +0,0 @@
{
"required": true,
"package": "dan200.computercraft.mixin",
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"injectors": {
"defaultRequire": 1
},
"mixins": [
"CacheUpdaterMixin"
],
"refmap": "computercraft.refmap.json"
}

View File

@@ -18,15 +18,18 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.platform.*;
import io.netty.buffer.Unpooled;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
@@ -47,12 +50,10 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiFunction;
@@ -129,31 +130,6 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
throw new UnsupportedOperationException("Cannot register ArgumentTypeInfo inside tests");
}
@Override
public void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player) {
throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
}
@Override
public void sendToPlayers(NetworkMessage<ClientNetworkContext> message, Collection<ServerPlayer> players) {
throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
}
@Override
public void sendToAllPlayers(NetworkMessage<ClientNetworkContext> message, MinecraftServer server) {
throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
}
@Override
public void sendToAllAround(NetworkMessage<ClientNetworkContext> message, ServerLevel level, Vec3 pos, float distance) {
throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
}
@Override
public void sendToAllTracking(NetworkMessage<ClientNetworkContext> message, LevelChunk chunk) {
throw new UnsupportedOperationException("Cannot send NetworkMessages inside tests");
}
@Override
public List<TagKey<Item>> getDyeTags() {
throw new UnsupportedOperationException("Cannot query tags inside tests");
@@ -169,20 +145,30 @@ 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);
record TypeImpl<T extends NetworkMessage<?>>(
ResourceLocation id, Function<FriendlyByteBuf, T> reader
) implements MessageType<T> {
}
@Override
public ComponentAccess<IPeripheral> createPeripheralAccess(Consumer<Direction> invalidate) {
public <T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
return new TypeImpl<>(channel, reader);
}
@Override
public Packet<ClientGamePacketListener> createPacket(NetworkMessage<ClientNetworkContext> message) {
var buf = new FriendlyByteBuf(Unpooled.buffer());
message.write(buf);
return new ClientboundCustomPayloadPacket(((TypeImpl<?>) message.type()).id(), buf);
}
@Override
public ComponentAccess<IPeripheral> createPeripheralAccess(BlockEntity owner, Consumer<Direction> invalidate) {
throw new UnsupportedOperationException("Cannot interact with the world inside tests");
}
@Override
public ComponentAccess<WiredElement> createWiredElementAccess(Consumer<Direction> invalidate) {
public ComponentAccess<WiredElement> createWiredElementAccess(BlockEntity owner, Consumer<Direction> invalidate) {
throw new UnsupportedOperationException("Cannot interact with the world inside tests");
}

View File

@@ -4,9 +4,10 @@
package dan200.computercraft.client.sound;
import io.netty.buffer.ByteBufAllocator;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class DfpwmStreamTest {
@@ -14,8 +15,7 @@ public class DfpwmStreamTest {
public void testDecodesBytes() {
var stream = new DfpwmStream();
var input = ByteBufAllocator.DEFAULT.buffer();
input.writeBytes(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
stream.push(input);
var buffer = stream.read(1024 + 1);

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.gametest
import dan200.computercraft.core.apis.FSAPI
import dan200.computercraft.core.util.Colour
import dan200.computercraft.gametest.api.*
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.media.items.DiskItem
@@ -19,6 +20,9 @@ import net.minecraft.network.chat.Component
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.block.RedStoneWireBlock
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.array
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Assertions.assertEquals
class Disk_Drive_Test {
@@ -45,14 +49,48 @@ class Disk_Drive_Test {
thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
}
/**
* A mount is initially attached, and then removed when the disk is ejected.
*/
@GameTest
fun Queues_event(helper: GameTestHelper) = helper.sequence {
val pos = BlockPos(1, 2, 2)
var started = false
var disk = false
var ejected = false
thenStartComputer {
// thenOnComputer discards events, so instead we need to track our state transitions.
started = true
val diskEvent = pullEvent("disk")
assertThat(diskEvent, array(equalTo("disk"), equalTo("right")))
disk = true
val ejectEvent = pullEvent("disk_eject")
assertThat(ejectEvent, array(equalTo("disk_eject"), equalTo("right")))
ejected = true
}
thenWaitUntil { helper.assertTrue(started, "Computer not started") }
thenExecute { helper.setContainerItem(pos, 0, ItemStack(Items.DIRT)) }
thenWaitUntil { helper.assertTrue(disk, "disk not inserted") }
thenExecute { helper.setContainerItem(pos, 0, ItemStack.EMPTY) }
thenWaitUntil { helper.assertTrue(ejected, "disk not ejected") }
}
/**
* A mount is initially attached, and then removed when the disk is ejected.
*/
@GameTest
fun Adds_removes_mount(helper: GameTestHelper) = helper.sequence {
thenOnComputer { } // Wait for the computer to start up
thenIdle(2) // Let the disk drive tick once to create the mount
thenOnComputer { // Then actually assert things!
thenExecute {
helper.setContainerItem(BlockPos(1, 2, 2), 0, DiskItem.createFromIDAndColour(1, null, Colour.BLACK.hex))
}
thenOnComputer {
getApi<FSAPI>().getDrive("disk").assertArrayEquals("right")
callPeripheral("right", "ejectDisk")
}

View File

@@ -24,7 +24,7 @@ class Inventory_Test {
*
* @see <https://github.com/cc-tweaked/cc-restitched/issues/121>
*/
@GameTest(required = false)
@GameTest
fun Checks_valid_item(helper: GameTestHelper) = helper.sequence {
thenOnComputer {
getApi<PeripheralAPI>().call(

View File

@@ -7,7 +7,10 @@ package dan200.computercraft.gametest
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.gametest.api.*
import dan200.computercraft.gametest.api.getBlockEntity
import dan200.computercraft.gametest.api.sequence
import dan200.computercraft.gametest.api.thenOnComputer
import dan200.computercraft.gametest.api.thenStartComputer
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
import dan200.computercraft.test.core.assertArrayEquals

View File

@@ -21,7 +21,6 @@ import net.minecraft.world.entity.EntityType
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import org.junit.jupiter.api.Assertions.*
import java.util.*
class Monitor_Test {
@GameTest

View File

@@ -19,7 +19,6 @@ import net.minecraft.world.inventory.MenuType
import net.minecraft.world.inventory.TransientCraftingContainer
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.item.crafting.CraftingRecipe
import net.minecraft.world.item.crafting.RecipeType
import org.junit.jupiter.api.Assertions.assertEquals
import java.util.*
@@ -37,11 +36,11 @@ class Recipe_Test {
container.setItem(0, ItemStack(Items.SKELETON_SKULL))
container.setItem(1, ItemStack(ModRegistry.Items.COMPUTER_ADVANCED.get()))
val recipe: Optional<CraftingRecipe> = context.level.server.recipeManager
val recipe = context.level.server.recipeManager
.getRecipeFor(RecipeType.CRAFTING, container, context.level)
if (!recipe.isPresent) throw GameTestAssertException("No recipe matches")
.orElseThrow { GameTestAssertException("No recipe matches") }
val result = recipe.get().assemble(container, context.level.registryAccess())
val result = recipe.assemble(container, context.level.registryAccess())
val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.gametest.api
import dan200.computercraft.api.peripheral.IPeripheral
import dan200.computercraft.gametest.core.ManagedComputers
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
@@ -21,6 +22,8 @@ import net.minecraft.world.Container
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.EntityType
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.entity.BarrelBlockEntity
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
@@ -165,6 +168,16 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
}
}
/**
* Get a [Container] at a given position.
*/
fun GameTestHelper.getContainerAt(pos: BlockPos): Container =
when (val container = getBlockEntity(pos)) {
is Container -> container
null -> failVerbose("Expected a container at $pos, found nothing", pos)
else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
}
/**
* Assert a container contains exactly these items and no more.
*
@@ -173,10 +186,7 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
* first `n` slots - the remaining are required to be empty.
*/
fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>) =
when (val container = getBlockEntity(pos) ?: failVerbose("Expected a container at $pos, found nothing", pos)) {
is Container -> assertContainerExactlyImpl(pos, container, items)
else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
}
assertContainerExactlyImpl(pos, getContainerAt(pos), items)
/**
* Assert an container contains exactly these items and no more.
@@ -206,9 +216,17 @@ private fun GameTestHelper.assertContainerExactlyImpl(pos: BlockPos, container:
}
}
/**
* A nasty hack to get a peripheral at a given position, by creating a dummy [BlockEntity].
*/
private fun GameTestHelper.getPeripheralAt(pos: BlockPos, direction: Direction): IPeripheral? {
val be = BarrelBlockEntity(absolutePos(pos).relative(direction), Blocks.BARREL.defaultBlockState())
be.setLevel(level)
return PlatformHelper.get().createPeripheralAccess(be) { }.get(direction.opposite)
}
fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) {
val peripheral = PlatformHelper.get().createPeripheralAccess { }
.get(level, absolutePos(pos).relative(direction), direction.opposite)
val peripheral = getPeripheralAt(pos, direction)
when {
peripheral == null -> fail("No peripheral at position", pos)
peripheral.type != type -> fail("Peripheral is of type ${peripheral.type}, expected $type", pos)
@@ -216,8 +234,7 @@ fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direct
}
fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) {
val peripheral = PlatformHelper.get().createPeripheralAccess { }
.get(level, absolutePos(pos).relative(direction), direction.opposite)
val peripheral = getPeripheralAt(pos, direction)
if (peripheral != null) fail("Expected no peripheral, got a ${peripheral.type}", pos)
}
@@ -277,3 +294,13 @@ fun GameTestHelper.setBlock(pos: BlockPos, state: BlockInput) = state.place(leve
fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
setBlock(pos, modify(getBlockState(pos)))
}
/**
* Update items in the container at [pos], setting the item in the specified [slot] to [item], and then marking it
* changed.
*/
fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
val container = getContainerAt(pos)
container.setItem(slot, item)
container.setChanged()
}

View File

@@ -34,7 +34,7 @@
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {Item: {Count: 1b, id: "computercraft:disk", tag: {Color: 1118481, DiskId: 0}}, id: "computercraft:disk_drive"}},
{pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {id: "computercraft:disk_drive"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},

View File

@@ -0,0 +1,138 @@
{
DataVersion: 2975,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {id: "computercraft:disk_drive"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:computer_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "disk_drive_test.queues_event", On: 1b, id: "computercraft:computer_advanced"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:air"},
{pos: [3, 1, 2], state: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{pos: [0, 2, 3], state: "minecraft:air"},
{pos: [0, 2, 4], state: "minecraft:air"},
{pos: [1, 2, 0], state: "minecraft:air"},
{pos: [1, 2, 1], state: "minecraft:air"},
{pos: [1, 2, 2], state: "minecraft:air"},
{pos: [1, 2, 3], state: "minecraft:air"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:air"},
{pos: [3, 2, 2], state: "minecraft:air"},
{pos: [3, 2, 3], state: "minecraft:air"},
{pos: [3, 2, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:disk_drive{facing:north,state:full}",
"computercraft:computer_advanced{facing:north,state:blinking}"
]
}

View File

@@ -50,13 +50,6 @@ tasks.test {
systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath())
}
tasks.testFixturesJar {
manifest {
// Ensure the test fixtures jar loads as a mod. Thanks FML >_>.
attributes("FMLModType" to "GAMELIBRARY")
}
}
val checkChangelog by tasks.registering(cc.tweaked.gradle.CheckChangelog::class) {
version.set(modVersion)
whatsNew.set(file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md"))

View File

@@ -100,8 +100,8 @@ public abstract class AbstractHandle {
/**
* Read a number of bytes from this file.
*
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
* may be 0 to determine we are at the end of the file.
* @param countArg The number of bytes to read. This may be 0 to determine we are at the end of the file. When
* absent, a single byte will be read.
* @return The read bytes.
* @throws LuaException When trying to read a negative number of bytes.
* @throws LuaException If the file has been closed.

View File

@@ -835,8 +835,8 @@ public final class ComputerThread implements ComputerScheduler {
var allocated = ThreadAllocations.getAllocatedBytes(current) - info.allocatedBytes();
if (allocated > 0) {
metrics.observe(Metrics.JAVA_ALLOCATION, allocated);
} else {
LOG.warn("Allocated a negative number of bytes!");
} else if (allocated < 0) {
LOG.warn("Allocated a negative number of bytes ({})!", allocated);
}
}

View File

@@ -1,64 +0,0 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.lua;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.methods.LuaMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.LuaError;
import org.squiddev.cobalt.LuaState;
import org.squiddev.cobalt.Varargs;
import org.squiddev.cobalt.function.VarArgFunction;
/**
* An "optimised" version of {@link ResultInterpreterFunction} which is guaranteed to never yield.
* <p>
* As we never yield, we do not need to push a function to the stack, which removes a small amount of overhead.
*/
class BasicFunction extends VarArgFunction {
private static final Logger LOG = LoggerFactory.getLogger(BasicFunction.class);
private final CobaltLuaMachine machine;
private final LuaMethod method;
private final Object instance;
private final ILuaContext context;
private final String funcName;
BasicFunction(CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name) {
this.machine = machine;
this.method = method;
this.instance = instance;
this.context = context;
funcName = name;
}
@Override
public Varargs invoke(LuaState luaState, Varargs args) throws LuaError {
var arguments = VarargArguments.of(args);
MethodResult results;
try {
results = method.apply(instance, context, arguments);
} catch (LuaException e) {
throw wrap(e);
} catch (Throwable t) {
LOG.error(Logging.JAVA_ERROR, "Error calling {} on {}", funcName, instance, t);
throw new LuaError("Java Exception Thrown: " + t, 0);
} finally {
arguments.close();
}
if (results.getCallback() != null) {
throw new IllegalStateException("Cannot have a yielding non-yielding function");
}
return machine.toValues(results.getResult());
}
public static LuaError wrap(LuaException exception) {
return exception.hasLevel() ? new LuaError(exception.getMessage()) : new LuaError(exception.getMessage(), exception.getLevel());
}
}

View File

@@ -164,9 +164,7 @@ public class CobaltLuaMachine implements ILuaMachine {
private LuaTable wrapLuaObject(Object object) {
var table = new LuaTable();
var found = luaMethods.forEachMethod(object, (target, name, method, info) ->
table.rawset(name, info != null && info.nonYielding()
? new BasicFunction(this, method, target, context, name)
: new ResultInterpreterFunction(this, method, target, context, name)));
table.rawset(name, new ResultInterpreterFunction(this, method, target, context, name)));
return found ? table : null;
}

View File

@@ -73,7 +73,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
}
@Override
protected Varargs resumeThis(LuaState state, Container container, Varargs args) throws LuaError, UnwindThrowable {
public Varargs resume(LuaState state, Container container, Varargs args) throws LuaError, UnwindThrowable {
MethodResult results;
var arguments = CobaltLuaMachine.toObjects(args);
try {
@@ -98,6 +98,6 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
if (!exception.hasLevel() && adjust == 0) return new LuaError(exception.getMessage());
var level = exception.getLevel();
return new LuaError(exception.getMessage(), level <= 0 ? level : level + adjust + 1);
return new LuaError(exception.getMessage(), level <= 0 ? level : level + adjust);
}
}

View File

@@ -1,3 +1,24 @@
# New features in CC: Tweaked 1.109.5
* Add a new `/computercraft-computer-folder` command to open a computer's folder
in singleplayer.
Several bug fixes:
* Discard characters being typed into the editor when closing `edit`'s `Run` screen.
# New features in CC: Tweaked 1.109.4
Several bug fixes:
* Don't log warnings when a computer allocates no bytes.
* Fix incorrect list index in command computer's NBT conversion (lonevox).
* Fix `endPage()` not updating the printer's block state.
* Several documentation improvements (znepb).
* Correctly mount disks before computer startup, not afterwards.
* Update to Cobalt 0.9
* Debug hooks are now correctly called for every function.
* Fix several minor inconsistencies with `debug.getinfo`.
* Fix Lua tables being sized incorrectly when created from varargs.
# New features in CC: Tweaked 1.109.3
* Command computers now display in the operator items creative tab.
@@ -6,7 +27,7 @@ Several bug fixes:
* Error if too many websocket messages are queued to be sent at once.
* Fix trailing-comma on method calls (e.g. `x:f(a, )` not using our custom error message.
* Fix internal compiler error when using `goto` as the first statement in an `if` block.
* Fix incorrect incorrect resizing of a tables' hash part when adding and removing keys.
* Fix incorrect resizing of a tables' hash part when adding and removing keys.
# New features in CC: Tweaked 1.109.2

View File

@@ -1,11 +1,9 @@
New features in CC: Tweaked 1.109.3
New features in CC: Tweaked 1.109.5
* Command computers now display in the operator items creative tab.
* Add a new `/computercraft-computer-folder` command to open a computer's folder
in singleplayer.
Several bug fixes:
* Error if too many websocket messages are queued to be sent at once.
* Fix trailing-comma on method calls (e.g. `x:f(a, )` not using our custom error message.
* Fix internal compiler error when using `goto` as the first statement in an `if` block.
* Fix incorrect incorrect resizing of a tables' hash part when adding and removing keys.
* Discard characters being typed into the editor when closing `edit`'s `Run` screen.
Type "help changelog" to see the full version history.

View File

@@ -0,0 +1,37 @@
-- SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
--[[- Utilities for working with events.
> [!DANGER]
> This is an internal module and SHOULD NOT be used in your own code. It may
> be removed or changed at any time.
@local
]]
--[[-
Attempt to discard a [`event!char`] event that may follow a [`event!key`] event.
This attempts to flush the event queue via a timer, stopping early if we observe
another key or char event.
We flush the event queue by waiting a single tick. It is technically possible
the key and char events will be delivered in different ticks, but it should be
very rare, and not worth adding extra delay for.
]]
local function discard_char()
local timer = os.startTimer(0)
while true do
local event, id = os.pullEvent()
if event == "timer" and id == timer then break
elseif event == "char" or event == "key" or event == "key_up" then
os.cancelTimer(timer)
break
end
end
end
return { discard_char = discard_char }

View File

@@ -88,6 +88,7 @@ for i = 1, #wrapped do
term.write(wrapped[i])
end
os.pullEvent('key')
require "cc.internal.event".discard_char()
]]
-- Menus

View File

@@ -300,7 +300,7 @@ local menu_choices = {
return false
end,
Exit = function()
sleep(0) -- Super janky, but consumes stray "char" events from pressing Ctrl then E separately.
require "cc.internal.event".discard_char() -- Consume stray "char" events from pressing Ctrl then E separately.
return true
end,
}

View File

@@ -257,7 +257,7 @@ while true do
offset = print_height - content_height
draw()
elseif param == keys.q then
sleep(0) -- Super janky, but consumes stray "char" events.
require "cc.internal.event".discard_char()
break
end
elseif event == "mouse_scroll" then

View File

@@ -67,8 +67,8 @@ public class MethodTest {
public void testPeripheralThrow() {
ComputerBootstrap.run(
"local throw = peripheral.wrap('top')\n" +
"local _, err = pcall(throw.thisThread) assert(err == 'pcall: !', err)\n" +
"local _, err = pcall(throw.mainThread) assert(err == 'pcall: !', err)",
"local _, err = pcall(function() throw.thisThread() end) assert(err == '/test.lua:2: !', (\"thisThread: %q\"):format(err))\n" +
"local _, err = pcall(function() throw.mainThread() end) assert(err == '/test.lua:3: !', (\"mainThread: %q\"):format(err))\n",
x -> x.getEnvironment().setPeripheral(ComputerSide.TOP, new PeripheralThrow()),
50
);

View File

@@ -0,0 +1,43 @@
-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
local timeout = require "test_helpers".timeout
describe("cc.internal.event", function()
local event = require "cc.internal.event"
describe("discard_char", function()
local function test(events)
local unique_event = "flush_" .. math.random(2 ^ 30)
-- Queue and pull to flush the queue once.
os.queueEvent(unique_event)
os.pullEvent(unique_event)
-- Queue our desired events
for i = 1, #events do os.queueEvent(table.unpack(events[i])) end
timeout(0.1, function()
event.discard_char()
-- Then read the remainder of the event queue, and check there's
-- no char event.
os.queueEvent(unique_event)
while true do
local event = os.pullEvent()
if event == unique_event then break end
expect(event):ne("char")
end
end)
end
it("discards char events", function()
test { { "char", "a" } }
end)
it("handles an empty event queue", function()
test {}
end)
end)
end)

View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.impl.client.ComputerCraftAPIClientService;
/**
* The Fabric-specific entrypoint for ComputerCraft's API.
*
* @see dan200.computercraft.api.ComputerCraftAPI The main API
* @see dan200.computercraft.api.client.ComputerCraftAPIClient The main client-side API
*/
public final class FabricComputerCraftAPIClient {
private FabricComputerCraftAPIClient() {
}
/**
* Register a {@link TurtleUpgradeModeller} for a class of turtle upgrades.
* <p>
* This may be called at any point after registry creation, though it is recommended to call it within your client
* setup step.
* <p>
* This method may be used as a {@link dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller}, for
* convenient use in multi-loader code.
*
* @param serialiser The turtle upgrade serialiser.
* @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade.
*/
public static <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
getInstance().registerTurtleUpgradeModeller(serialiser, modeller);
}
private static ComputerCraftAPIClientService getInstance() {
return ComputerCraftAPIClientService.get();
}
}

View File

@@ -46,9 +46,25 @@ fun addRemappedConfiguration(name: String) {
addRemappedConfiguration("testWithSodium")
addRemappedConfiguration("testWithIris")
configurations {
// Declare some configurations which are both included (jar-in-jar-ed) and a normal dependency (so they appear in
// our POM).
val includeRuntimeOnly by registering {
isCanBeConsumed = false
isCanBeResolved = false
}
val includeImplementation by registering {
isCanBeConsumed = false
isCanBeResolved = false
}
include { extendsFrom(includeRuntimeOnly.get(), includeImplementation.get()) }
runtimeOnly { extendsFrom(includeRuntimeOnly.get()) }
implementation { extendsFrom(includeImplementation.get()) }
}
dependencies {
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
modImplementation(libs.bundles.externalMods.fabric) { cct.exclude(this) }
modCompileOnly(libs.bundles.externalMods.fabric.compile) {
exclude("net.fabricmc", "fabric-loader")
exclude("net.fabricmc.fabric-api")
@@ -63,24 +79,19 @@ dependencies {
"modTestWithIris"(libs.iris)
"modTestWithIris"(libs.sodium)
include(libs.cobalt)
include(libs.jzlib)
include(libs.netty.http)
include(libs.netty.socks)
include(libs.netty.proxy)
include(libs.nightConfig.core)
include(libs.nightConfig.toml)
"includeRuntimeOnly"(libs.cobalt)
"includeRuntimeOnly"(libs.jzlib)
"includeRuntimeOnly"(libs.netty.http)
"includeRuntimeOnly"(libs.netty.socks)
"includeRuntimeOnly"(libs.netty.proxy)
"includeImplementation"(libs.nightConfig.core)
"includeImplementation"(libs.nightConfig.toml)
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
api(commonClasses(project(":fabric-api"))) { cct.exclude(this) }
clientApi(clientClasses(project(":fabric-api"))) { cct.exclude(this) }
implementation(project(":core")) { cct.exclude(this) }
// These are transitive deps of :core, so we don't need these deps. However, we want them to appear as runtime deps
// in our POM, and this is the easiest way.
runtimeOnly(libs.cobalt)
runtimeOnly(libs.netty.http)
runtimeOnly(libs.netty.socks)
runtimeOnly(libs.netty.proxy)
annotationProcessorEverywhere(libs.autoService)
@@ -135,7 +146,6 @@ loom {
client()
runDir("run/dataGen")
property("cct.pretty-json")
property("fabric-api.datagen")
property("fabric-api.datagen.output-dir", file("src/generated/resources").absolutePath)
property("fabric-api.datagen.strict-validation")

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.client;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.FabricComputerCraftAPIClient;
import dan200.computercraft.client.model.CustomModelLoader;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.NetworkMessages;
@@ -14,6 +16,8 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.platform.FabricMessageType;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
@@ -31,13 +35,15 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class ComputerCraftClient {
public static void init() {
var clientNetwork = Services.load(ClientNetworkContext.class);
for (var type : NetworkMessages.getClientbound()) {
ClientPlayNetworking.registerGlobalReceiver(
FabricMessageType.toFabricType(type), (packet, player, responseSender) -> packet.payload().handle(ClientNetworkContext.get())
FabricMessageType.toFabricType(type), (packet, player, responseSender) -> packet.payload().handle(clientNetwork)
);
}
ClientRegistry.register();
ClientRegistry.registerTurtleModellers(FabricComputerCraftAPIClient::registerTurtleUpgradeModeller);
ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register);
ClientRegistry.registerMainThread();
@@ -77,6 +83,10 @@ public class ComputerCraftClient {
return cable.getCloneItemStack(state, hit, level, pos, player);
});
ClientCommandRegistrationCallback.EVENT.register(
(dispatcher, registryAccess) -> ClientRegistry.registerClientCommands(dispatcher, FabricClientCommandSource::sendError)
);
((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml"));
}
}

View File

@@ -1,24 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
@AutoService(ClientNetworkContext.class)
public class ClientNetworkHandlerImpl extends AbstractClientNetworkContext {
@Override
public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name) {
var mc = Minecraft.getInstance();
mc.levelRenderer.playStreamingMusic(sound, pos);
if (name != null) mc.gui.setNowPlaying(Component.literal(name));
}
}

View File

@@ -12,13 +12,19 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import dan200.computercraft.shared.platform.FabricMessageType;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerGamePacketListener;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.util.RandomSource;
import javax.annotation.Nullable;
@@ -28,8 +34,10 @@ public class ClientPlatformHelperImpl implements ClientPlatformHelper {
private static final RandomSource random = RandomSource.create(0);
@Override
public void sendToServer(NetworkMessage<ServerNetworkContext> message) {
ClientPlayNetworking.send(FabricMessageType.toFabricPacket(message));
public Packet<ServerGamePacketListener> createPacket(NetworkMessage<ServerNetworkContext> message) {
var buf = PacketByteBufs.create();
message.write(buf);
return ClientPlayNetworking.createC2SPacket(FabricMessageType.toFabricType(message.type()).getId(), buf);
}
@Override
@@ -55,4 +63,9 @@ public class ClientPlatformHelperImpl implements ClientPlatformHelper {
ModelRenderer.renderQuads(transform, buffer, model.getQuads(null, face, random), lightmapCoord, overlayLight, tints);
}
}
@Override
public void playStreamingMusic(BlockPos pos, @Nullable SoundEvent sound) {
Minecraft.getInstance().levelRenderer.playStreamingMusic(sound, pos);
}
}

View File

@@ -98,7 +98,7 @@
"gui.computercraft.config.http.max_requests": "Maximum concurrent requests",
"gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0",
"gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets",
"gui.computercraft.config.http.max_websockets.tooltip": "The number of websockets a computer can have open at one time. Set to 0 for unlimited.\nRange: > 1",
"gui.computercraft.config.http.max_websockets.tooltip": "The number of websockets a computer can have open at one time.\nRange: > 1",
"gui.computercraft.config.http.proxy": "Proxy",
"gui.computercraft.config.http.proxy.host": "Host name",
"gui.computercraft.config.http.proxy.host.tooltip": "The hostname or IP address of the proxy server.",

View File

@@ -36,20 +36,28 @@ import java.util.function.Consumer;
public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override
public void onInitializeDataGenerator(FabricDataGenerator generator) {
var pack = generator.createPack();
DataProviders.add(new PlatformGeneratorsImpl(pack));
pack.addProvider((out, reg) -> addName("Conventional Tags", new MoreConventionalTagsProvider(out, reg)));
var pack = new PlatformGeneratorsImpl(generator.createPack());
DataProviders.add(pack);
pack.addWithRegistries((out, reg) -> addName("Conventional Tags", new MoreConventionalTagsProvider(out, reg)));
}
private record PlatformGeneratorsImpl(FabricDataGenerator.Pack generator) implements DataProviders.GeneratorSink {
public <T extends DataProvider> T addWithFabricOutput(FabricDataGenerator.Pack.Factory<T> factory) {
return generator.addProvider((FabricDataOutput p) -> new PrettyDataProvider<>(factory.create(p))).provider();
}
public <T extends DataProvider> T addWithRegistries(FabricDataGenerator.Pack.RegistryDependentFactory<T> factory) {
return generator.addProvider((r, p) -> new PrettyDataProvider<>(factory.create(r, p))).provider();
}
@Override
public <T extends DataProvider> T add(DataProvider.Factory<T> factory) {
return generator.addProvider(factory);
return generator.addProvider((PackOutput p) -> new PrettyDataProvider<>(factory.create(p))).provider();
}
@Override
public <T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output) {
generator.addProvider((FabricDataOutput out) -> {
addWithFabricOutput((FabricDataOutput out) -> {
var ourType = switch (type) {
case SERVER_DATA -> PackOutput.Target.DATA_PACK;
case CLIENT_RESOURCES -> PackOutput.Target.RESOURCE_PACK;
@@ -71,7 +79,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override
public void lootTable(List<LootTableProvider.SubProviderEntry> tables) {
for (var table : tables) {
generator.addProvider((FabricDataOutput out) -> new SimpleFabricLootTableProvider(out, table.paramSet()) {
addWithFabricOutput((FabricDataOutput out) -> new SimpleFabricLootTableProvider(out, table.paramSet()) {
@Override
public void generate(BiConsumer<ResourceLocation, LootTable.Builder> exporter) {
table.provider().get().generate(exporter);
@@ -82,7 +90,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override
public TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags) {
return generator.addProvider((out, registries) -> new FabricTagProvider.BlockTagProvider(out, registries) {
return addWithRegistries((out, registries) -> new FabricTagProvider.BlockTagProvider(out, registries) {
@Override
protected void addTags(HolderLookup.Provider registries) {
tags.accept(x -> new TagProvider.TagAppender<>(RegistryWrappers.BLOCKS, getOrCreateRawBuilder(x)));
@@ -92,7 +100,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override
public TagsProvider<Item> itemTags(Consumer<TagProvider.ItemTagConsumer> tags, TagsProvider<Block> blocks) {
return generator.addProvider((out, registries) -> new FabricTagProvider.ItemTagProvider(out, registries, (FabricTagProvider.BlockTagProvider) blocks) {
return addWithRegistries((out, registries) -> new FabricTagProvider.ItemTagProvider(out, registries, (FabricTagProvider.BlockTagProvider) blocks) {
@Override
protected void addTags(HolderLookup.Provider registries) {
var self = this;
@@ -113,7 +121,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override
public void models(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) {
generator.addProvider((FabricDataOutput out) -> new FabricModelProvider(out) {
addWithFabricOutput((FabricDataOutput out) -> new FabricModelProvider(out) {
@Override
public void generateBlockStateModels(BlockModelGenerators generator) {
blocks.accept(generator);

View File

@@ -9,7 +9,7 @@ import dan200.computercraft.shared.peripheral.generic.ComponentLookup;
import dan200.computercraft.shared.peripheral.generic.GenericPeripheralProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.Level;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
import javax.annotation.Nullable;
@@ -29,7 +29,7 @@ public final class Peripherals {
genericProvider.registerLookup(lookup);
}
public static @Nullable IPeripheral getGenericPeripheral(Level level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, Runnable invalidate) {
public static @Nullable IPeripheral getGenericPeripheral(ServerLevel level, BlockPos pos, Direction side, @Nullable BlockEntity blockEntity, Runnable invalidate) {
return genericProvider.getPeripheral(level, pos, side, blockEntity, invalidate);
}
}

View File

@@ -15,6 +15,7 @@ import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.details.FluidDetails;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.network.client.UpgradesLoadedMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.peripheral.commandblock.CommandBlockPeripheral;
import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlockEntity;
@@ -22,7 +23,6 @@ import dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlockEnt
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockEntity;
import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.platform.FabricMessageType;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
@@ -92,7 +92,7 @@ public class ComputerCraft {
CommonHooks.onServerStopped();
((FabricConfigFile) ConfigSpec.serverSpec).unload();
});
ServerLifecycleEvents.SYNC_DATA_PACK_CONTENTS.register((player, joined) -> PlatformHelper.get().sendToPlayer(new UpgradesLoadedMessage(), player));
ServerLifecycleEvents.SYNC_DATA_PACK_CONTENTS.register((player, joined) -> ServerNetworking.sendToPlayer(new UpgradesLoadedMessage(), player));
ServerTickEvents.START_SERVER_TICK.register(CommonHooks::onServerTickStart);
ServerTickEvents.START_SERVER_TICK.register(s -> CommonHooks.onServerTickEnd());

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