1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-14 20:17:11 +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 /logs
/build /build
/projects/*/logs /projects/*/logs
/projects/fabric/fabricloader.log
/projects/*/build /projects/*/build
/buildSrc/build /buildSrc/build
/out /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 You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file
exposing more features. an issue to let me know!
We bundle the API sources with the jar, so documentation should be easily viewable within your editor. Alternatively, 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/). 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/") { maven("https://maven.fabricmc.net/") {
name = "Fabric" name = "Fabric"
content { content {
includeGroup("net.fabricmc") includeGroup("net.fabricmc")
} }
} }
maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
includeGroup("cc.tweaked.vanilla-extract")
}
}
} }
dependencies { dependencies {
@@ -55,8 +55,7 @@ dependencies {
implementation(libs.ideaExt) implementation(libs.ideaExt)
implementation(libs.librarian) implementation(libs.librarian)
implementation(libs.minotaur) implementation(libs.minotaur)
implementation(libs.vanillaGradle) implementation(libs.vanillaExtract)
implementation(libs.vineflower)
} }
gradlePlugin { gradlePlugin {

View File

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

View File

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

View File

@@ -7,12 +7,15 @@ package cc.tweaked.gradle
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration 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.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentSelector import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.DependencyResult import org.gradle.api.artifacts.result.DependencyResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.provider.ListProperty 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.Input
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.language.base.plugins.LifecycleBasePlugin
@@ -21,9 +24,25 @@ abstract class DependencyCheck : DefaultTask() {
@get:Input @get:Input
abstract val configuration: ListProperty<Configuration> 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 { init {
description = "Check :core's dependencies are consistent with Minecraft's." description = "Check :core's dependencies are consistent with Minecraft's."
group = LifecycleBasePlugin.VERIFICATION_GROUP 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 @TaskAction
@@ -60,7 +79,8 @@ abstract class DependencyCheck : DefaultTask() {
) { ) {
// If the version is different between the requested and selected version, report an error. // If the version is different between the requested and selected version, report an error.
val selected = dependency.selected.moduleVersion!!.version 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) logger.error("Requested dependency {} (via {}) but got version {}", requested, from, selected)
return false return false
} }

View File

@@ -4,23 +4,17 @@
package cc.tweaked.gradle 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.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ModuleDependency import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.dsl.DependencyHandler 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.BasePlugin
import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.kotlin.dsl.get 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 * 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) } 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) { project.tasks.register(client.jarTaskName, Jar::class.java) {
description = "An empty jar standing in for the client classes." description = "An empty jar standing in for the client classes."
group = BasePlugin.BUILD_GROUP group = BasePlugin.BUILD_GROUP
archiveClassifier.set("client") archiveClassifier.set("client")
} }
setupOutgoing(client)
MinecraftSetup(project).setupOutgoingConfigurations()
// Reset the client classpath (Loom configures it slightly differently to this) and add a main -> client // 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, // 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("jar", Jar::class.java) { from(client.output) }
project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) } project.tasks.named("sourcesJar", Jar::class.java) { from(client.allSource) }
setupBasic()
}
private fun setupBasic() {
val client = sourceSets["client"]
project.extensions.configure(CCTweakedExtension::class.java) { project.extensions.configure(CCTweakedExtension::class.java) {
sourceDirectories.add(SourceSetReference.internal(client)) sourceDirectories.add(SourceSetReference.internal(client))
} }
@@ -120,83 +102,19 @@ class MinecraftConfigurations private constructor(private val project: Project)
project.tasks.named("check") { dependsOn(checkDependencyConsistency) } 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 { companion object {
fun setupBasic(project: Project) {
MinecraftConfigurations(project).setupBasic()
}
fun setup(project: Project) { fun setup(project: Project) {
MinecraftConfigurations(project).setup() MinecraftConfigurations(project).setup()
} }
} }
} }
private class BasicIncomingCapability(private val module: ModuleDependency, private val name: String) : Capability { fun DependencyHandler.clientClasses(notation: Any): ModuleDependency =
override fun getGroup(): String = module.group!! Capabilities.clientClasses(create(notation) as ModuleDependency)
override fun getName(): String = "${module.name}-$name"
override fun getVersion(): String? = null
}
private class BasicOutgoingCapability(private val project: Project, private val name: String) : Capability { fun DependencyHandler.commonClasses(notation: Any): ModuleDependency =
override fun getGroup(): String = project.group.toString() Capabilities.commonClasses(create(notation) as ModuleDependency)
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
}

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 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. 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: - Update to Lua 5.2:
- Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs. - 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, - 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`. 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 [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" [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 # Mod properties
isUnstable=false 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 # Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.20.1 mcVersion=1.20.1

View File

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

Binary file not shown.

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

22
gradlew vendored
View File

@@ -83,7 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -130,10 +131,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java 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 Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-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-highlight": "^7.0.0",
"rehype-react": "^8.0.0", "rehype-react": "^8.0.0",
"rollup": "^4.0.0", "rollup": "^4.0.0",
"tsx": "^3.12.10", "tsx": "^4.7.0",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
} }

View File

@@ -27,8 +27,13 @@ public final class ComputerCraftAPIClient {
* @param serialiser The turtle upgrade serialiser. * @param serialiser The turtle upgrade serialiser.
* @param modeller The upgrade modeller. * @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade. * @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) { public static <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
// TODO(1.20.4): Remove this
getInstance().registerTurtleUpgradeModeller(serialiser, modeller); 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; package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.client.ComputerCraftAPIClient;
import dan200.computercraft.api.client.TransformedModel; import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
@@ -21,9 +19,13 @@ import java.util.List;
/** /**
* Provides models for a {@link ITurtleUpgrade}. * 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. * @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> { public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
/** /**

View File

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

View File

@@ -22,6 +22,14 @@ configurations {
register("cctJavadoc") register("cctJavadoc")
} }
repositories {
maven("https://maven.minecraftforge.net/") {
content {
includeModule("org.spongepowered", "mixin")
}
}
}
dependencies { dependencies {
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness. // Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
implementation(project(":core")) implementation(project(":core"))
@@ -31,7 +39,6 @@ dependencies {
compileOnly(libs.bundles.externalMods.common) compileOnly(libs.bundles.externalMods.common)
clientCompileOnly(variantOf(libs.emi) { classifier("api") }) clientCompileOnly(variantOf(libs.emi) { classifier("api") })
compileOnly(libs.mixin)
annotationProcessorEverywhere(libs.autoService) annotationProcessorEverywhere(libs.autoService)
testFixturesAnnotationProcessor(libs.autoService) testFixturesAnnotationProcessor(libs.autoService)
@@ -39,6 +46,7 @@ dependencies {
testImplementation(libs.bundles.test) testImplementation(libs.bundles.test)
testRuntimeOnly(libs.bundles.testRuntime) testRuntimeOnly(libs.bundles.testRuntime)
testModCompileOnly(libs.mixin)
testModImplementation(testFixtures(project(":core"))) testModImplementation(testFixtures(project(":core")))
testModImplementation(testFixtures(project(":common"))) testModImplementation(testFixtures(project(":common")))
testModImplementation(libs.bundles.kotlin) testModImplementation(libs.bundles.kotlin)
@@ -72,7 +80,7 @@ val luaJavadoc by tasks.registering(Javadoc::class) {
javadocTool.set( javadocTool.set(
javaToolchains.javadocToolFor { 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.client.sound.SpeakerManager;
import dan200.computercraft.shared.CommonHooks; import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.ModRegistry; 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.media.items.PrintoutItem;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock; import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant; 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.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.PauseAwareTimer; import dan200.computercraft.shared.util.PauseAwareTimer;
import dan200.computercraft.shared.util.WorldUtil; import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.Util;
import net.minecraft.client.Camera; import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.MultiBufferSource;
@@ -43,7 +40,6 @@ import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.HitResult;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.File;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -71,10 +67,6 @@ public final class ClientHooks {
ClientPocketComputers.reset(); ClientPocketComputers.reset();
} }
public static boolean onChatMessage(String message) {
return handleOpenComputerCommand(message);
}
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) { public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit) return CableHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit)
|| MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit); || MonitorHighlightRenderer.drawHighlight(transform, bufferSource, camera, hit);
@@ -109,34 +101,6 @@ public final class ClientHooks {
SpeakerManager.onPlayStreaming(engine, channel, stream); 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. * Add additional information about the currently targeted block to the debug screen.
* *

View File

@@ -4,8 +4,12 @@
package dan200.computercraft.client; 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.ComputerCraftAPI;
import dan200.computercraft.api.client.ComputerCraftAPIClient; import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller; import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.client.gui.*; import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.pocket.ClientPocketComputers; 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.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.core.util.Colour; import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.common.IColouredItem; 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.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem; import dan200.computercraft.shared.media.items.TreasureDiskItem;
import net.minecraft.Util;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemColor; import net.minecraft.client.color.item.ItemColor;
import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.gui.screens.MenuScreens;
@@ -30,6 +37,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction; import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceProvider; import net.minecraft.server.packs.resources.ResourceProvider;
@@ -39,6 +47,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.ItemLike;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; 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. * Register any client-side objects which don't have to be done on the main thread.
*/ */
public static void register() { 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_NORMAL.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new); BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::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 @SafeVarargs
private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) { private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name); var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
@@ -179,4 +190,45 @@ public final class ClientRegistry {
return function.unclampedCall(stack, level, entity, layer); 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.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.DynamicImageButton; import dan200.computercraft.client.gui.widgets.DynamicImageButton;
import dan200.computercraft.client.gui.widgets.TerminalWidget; 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.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.InputHandler; import dan200.computercraft.shared.computer.core.InputHandler;
@@ -207,7 +207,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
return; 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) { public void uploadResult(UploadResult result, @Nullable Component message) {

View File

@@ -4,7 +4,7 @@
package dan200.computercraft.client.gui; 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.core.InputHandler;
import dan200.computercraft.shared.computer.menu.ComputerMenu; import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage; import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
@@ -29,51 +29,51 @@ public final class ClientInputHandler implements InputHandler {
@Override @Override
public void turnOn() { public void turnOn() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON)); ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
} }
@Override @Override
public void shutdown() { public void shutdown() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN)); ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.SHUTDOWN));
} }
@Override @Override
public void reboot() { public void reboot() {
ClientPlatformHelper.get().sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT)); ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
} }
@Override @Override
public void queueEvent(String event, @Nullable Object[] arguments) { public void queueEvent(String event, @Nullable Object[] arguments) {
ClientPlatformHelper.get().sendToServer(new QueueEventServerMessage(menu, event, arguments)); ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
} }
@Override @Override
public void keyDown(int key, boolean repeat) { 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 @Override
public void keyUp(int key) { 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 @Override
public void mouseClick(int button, int x, int y) { 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 @Override
public void mouseUp(int button, int x, int y) { 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 @Override
public void mouseDrag(int button, int x, int y) { 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 @Override
public void mouseScroll(int direction, int x, int y) { 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; package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import dan200.computercraft.client.ClientTableFormatter; import dan200.computercraft.client.ClientTableFormatter;
import dan200.computercraft.client.gui.AbstractComputerScreen; import dan200.computercraft.client.gui.AbstractComputerScreen;
import dan200.computercraft.client.gui.OptionScreen; 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.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity; import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
/** /**
* The base implementation of {@link ClientNetworkContext}. * The client-side implementation of {@link ClientNetworkContext}.
* <p>
* This should be extended by mod loader specific modules with the remaining abstract methods.
*/ */
public abstract class AbstractClientNetworkContext implements ClientNetworkContext { @AutoService(ClientNetworkContext.class)
public final class ClientNetworkContextImpl implements ClientNetworkContext {
@Override @Override
public final void handleChatTable(TableBuilder table) { public void handleChatTable(TableBuilder table) {
ClientTableFormatter.INSTANCE.display(table); ClientTableFormatter.INSTANCE.display(table);
} }
@Override @Override
public final void handleComputerTerminal(int containerId, TerminalState terminal) { public void handleComputerTerminal(int containerId, TerminalState terminal) {
Player player = Minecraft.getInstance().player; Player player = Minecraft.getInstance().player;
if (player != null && player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) { if (player != null && player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
menu.updateTerminal(terminal); menu.updateTerminal(terminal);
@@ -48,7 +49,7 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
} }
@Override @Override
public final void handleMonitorData(BlockPos pos, TerminalState terminal) { public void handleMonitorData(BlockPos pos, TerminalState terminal) {
var player = Minecraft.getInstance().player; var player = Minecraft.getInstance().player;
if (player == null) return; if (player == null) return;
@@ -59,44 +60,46 @@ public abstract class AbstractClientNetworkContext implements ClientNetworkConte
} }
@Override @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); var computer = ClientPocketComputers.get(instanceId, terminal.colour);
computer.setState(state, lightState); computer.setState(state, lightState);
if (terminal.hasTerminal()) computer.setTerminal(terminal); if (terminal.hasTerminal()) computer.setTerminal(terminal);
} }
@Override @Override
public final void handlePocketComputerDeleted(int instanceId) { public void handlePocketComputerDeleted(int instanceId) {
ClientPocketComputers.remove(instanceId); ClientPocketComputers.remove(instanceId);
} }
@Override @Override
public final void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume) { public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume); SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
} }
@Override @Override
public final void handleSpeakerAudioPush(UUID source, ByteBuf buffer) { public void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
SpeakerManager.getSound(source).pushAudio(buffer);
}
@Override
public final void handleSpeakerMove(UUID source, SpeakerPosition.Message position) {
SpeakerManager.moveSound(source, reifyPosition(position)); SpeakerManager.moveSound(source, reifyPosition(position));
} }
@Override @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); SpeakerManager.getSound(source).playSound(reifyPosition(position), sound, volume, pitch);
} }
@Override @Override
public final void handleSpeakerStop(UUID source) { public void handleSpeakerStop(UUID source) {
SpeakerManager.stopSound(source); SpeakerManager.stopSound(source);
} }
@Override @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 minecraft = Minecraft.getInstance();
var screen = OptionScreen.unwrap(minecraft.screen); 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 dan200.computercraft.shared.network.server.ServerNetworkContext;
import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.model.BakedModel; 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; 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. * 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. * @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); 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 com.mojang.blaze3d.audio.Channel;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; 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.AudioStream;
import net.minecraft.client.sounds.SoundEngine; import net.minecraft.client.sounds.SoundEngine;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
@@ -36,7 +36,7 @@ class DfpwmStream implements AudioStream {
/** /**
* The {@link Channel} which this sound is playing on. * The {@link Channel} which this sound is playing on.
* *
* @see SpeakerInstance#pushAudio(ByteBuf) * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
*/ */
@Nullable @Nullable
Channel channel; Channel channel;
@@ -44,7 +44,7 @@ class DfpwmStream implements AudioStream {
/** /**
* The underlying {@link SoundEngine} executor. * The underlying {@link SoundEngine} executor.
* *
* @see SpeakerInstance#pushAudio(ByteBuf) * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
* @see SoundEngine#executor * @see SoundEngine#executor
*/ */
@Nullable @Nullable
@@ -58,12 +58,12 @@ class DfpwmStream implements AudioStream {
DfpwmStream() { DfpwmStream() {
} }
void push(ByteBuf input) { void push(ByteBuffer input) {
var readable = input.readableBytes(); var readable = input.remaining();
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder()); var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());
for (var i = 0; i < readable; i++) { for (var i = 0; i < readable; i++) {
var inputByte = input.readByte(); var inputByte = input.get();
for (var j = 0; j < 8; j++) { for (var j = 0; j < 8; j++) {
var currentBit = (inputByte & 1) != 0; var currentBit = (inputByte & 1) != 0;
var target = currentBit ? 127 : -128; var target = currentBit ? 127 : -128;

View File

@@ -7,11 +7,11 @@ package dan200.computercraft.client.sound;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.core.util.Nullability; import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; 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. * 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() { SpeakerInstance() {
} }
public synchronized void pushAudio(ByteBuf buffer) { private void pushAudio(ByteBuffer buffer) {
var sound = this.sound; var sound = this.sound;
var stream = currentStream; 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(); var soundManager = Minecraft.getInstance().getSoundManager();
if (sound != null && sound.stream != currentStream) { 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.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.impl.PlatformHelper;
import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager; import dan200.computercraft.impl.UpgradeManager;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
@@ -24,14 +27,15 @@ import java.util.stream.Stream;
/** /**
* A registry of {@link TurtleUpgradeModeller}s. * A registry of {@link TurtleUpgradeModeller}s.
*
* @see dan200.computercraft.api.client.ComputerCraftAPIClient#registerTurtleUpgradeModeller(TurtleUpgradeSerialiser, TurtleUpgradeModeller)
*/ */
public final class TurtleUpgradeModellers { public final class TurtleUpgradeModellers {
private static final Logger LOG = LoggerFactory.getLogger(TurtleUpgradeModellers.class);
private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side) -> private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side) ->
new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity()); new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
private static final Map<TurtleUpgradeSerialiser<?>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>(); 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 * 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) { public static <T extends ITurtleUpgrade> void register(TurtleUpgradeSerialiser<T> serialiser, TurtleUpgradeModeller<T> modeller) {
synchronized (turtleModels) { if (fetchedModels) {
if (turtleModels.containsKey(serialiser)) { // TODO(1.20.4): Replace with an error.
throw new IllegalStateException("Modeller already registered for serialiser"); 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() { public static Stream<ResourceLocation> getDependencies() {
fetchedModels = true;
return turtleModels.values().stream().flatMap(x -> x.getDependencies().stream()); 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. * 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 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 Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
private static final int MAX_WIDTH = 120; private static final int MAX_WIDTH = 120;
@@ -44,17 +43,6 @@ public class PrettyJsonWriter extends JsonWriter {
this.out = out; 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. * Reformat a JSON string with our pretty printer.
* *
@@ -62,8 +50,6 @@ public class PrettyJsonWriter extends JsonWriter {
* @return The reformatted string. * @return The reformatted string.
*/ */
public static byte[] reformat(byte[] contents) { public static byte[] reformat(byte[] contents) {
if (!ENABLED) return contents;
JsonElement object; JsonElement object;
try (var reader = new InputStreamReader(new ByteArrayInputStream(contents), StandardCharsets.UTF_8)) { try (var reader = new InputStreamReader(new ByteArrayInputStream(contents), StandardCharsets.UTF_8)) {
object = GSON.fromJson(reader, JsonElement.class); 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, // TODO: Can we track which mod this resource came from and use that instead? It's theoretically possible,
// but maybe not ideal for datapacks. // but maybe not ideal for datapacks.
var modId = id.getNamespace(); 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); var upgrade = serialiser.fromJson(id, root);
if (!upgrade.getUpgradeID().equals(id)) { 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 final class CommandComputerCraft {
public static final UUID SYSTEM_UUID = new UUID(0, 0); 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() { private CommandComputerCraft() {
} }
@@ -389,7 +394,7 @@ public final class CommandComputerCraft {
return link( return link(
text("\u270E"), text("\u270E"),
"/" + OPEN_COMPUTER + id, "/" + CLIENT_OPEN_FOLDER + " " + id,
Component.translatable("commands.computercraft.dump.open_path") 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.core.util.Nullability;
import dan200.computercraft.shared.command.CommandUtils; import dan200.computercraft.shared.command.CommandUtils;
import dan200.computercraft.shared.network.client.ChatTableClientMessage; 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.commands.CommandSourceStack;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
@@ -105,7 +105,7 @@ public class TableBuilder {
if (CommandUtils.isPlayer(source)) { if (CommandUtils.isPlayer(source)) {
trim(18); trim(18);
var player = (ServerPlayer) Nullability.assertNonNull(source.getEntity()); var player = (ServerPlayer) Nullability.assertNonNull(source.getEntity());
PlatformHelper.get().sendToPlayer(new ChatTableClientMessage(this), player); ServerNetworking.sendToPlayer(new ChatTableClientMessage(this), player);
} else { } else {
trim(100); trim(100);
new ServerTableFormatter(source).display(this); new ServerTableFormatter(source).display(this);

View File

@@ -25,7 +25,6 @@ import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.*; import net.minecraft.world.*;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@@ -51,7 +50,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
private boolean fresh = false; private boolean fresh = false;
private int invalidSides = 0; 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; private LockCode lockCode = LockCode.NO_LOCK;
@@ -218,7 +217,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
var localDir = remapToLocalSide(dir); var localDir = remapToLocalSide(dir);
if (isPeripheralBlockedOnSide(localDir)) return; if (isPeripheralBlockedOnSide(localDir)) return;
var peripheral = peripherals.get((ServerLevel) getLevel(), getBlockPos(), dir); var peripheral = peripherals.get(dir);
computer.setPeripheral(localDir, peripheral); 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.Vec2;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import java.util.HashMap; import java.util.ArrayList;
import java.util.Map; import java.util.List;
public class CommandComputerBlockEntity extends ComputerBlockEntity { public class CommandComputerBlockEntity extends ComputerBlockEntity {
public class CommandReceiver implements CommandSource { public class CommandReceiver implements CommandSource {
private final Map<Integer, String> output = new HashMap<>(); private final List<String> output = new ArrayList<>();
public void clearOutput() { public void clearOutput() {
output.clear(); output.clear();
} }
public Map<Integer, String> getOutput() { public List<String> copyOutput() {
return output; return new ArrayList<>(output);
}
public Map<Integer, String> copyOutput() {
return new HashMap<>(output);
} }
@Override @Override
public void sendSystemMessage(Component textComponent) { public void sendSystemMessage(Component textComponent) {
output.put(output.size() + 1, textComponent.getString()); output.add(textComponent.getString());
} }
@Override @Override

View File

@@ -20,7 +20,7 @@ import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext; import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage; 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.core.BlockPos;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -142,7 +142,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
for (var player : server.getPlayerList().getPlayers()) { for (var player : server.getPlayerList().getPlayers()) {
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) { 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.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.UploadResultMessage; 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.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@@ -138,7 +138,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
return; return;
} }
PlatformHelper.get().sendToPlayer(finishUpload(uploader), uploader); ServerNetworking.sendToPlayer(finishUpload(uploader), uploader);
} }
private UploadResultMessage finishUpload(ServerPlayer player) { 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(), toUpload.stream().map(x -> new TransferredFile(x.getName(), new ByteBufferChannel(x.getBytes()))).toList(),
() -> { () -> {
if (player.isAlive() && player.containerMenu == owner) { 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); .defineInRange("max_requests", CoreConfig.httpMaxRequests, 0, Integer.MAX_VALUE);
httpMaxWebsockets = builder 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); .defineInRange("max_websockets", CoreConfig.httpMaxWebsockets, 1, Integer.MAX_VALUE);
builder builder

View File

@@ -4,30 +4,24 @@
package dan200.computercraft.shared.network.client; package dan200.computercraft.shared.network.client;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import io.netty.buffer.ByteBuf;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
/** /**
* The context under which clientbound packets are evaluated. * The context under which clientbound packets are evaluated.
*/ */
public interface ClientNetworkContext { 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 handleChatTable(TableBuilder table);
void handleComputerTerminal(int containerId, TerminalState terminal); void handleComputerTerminal(int containerId, TerminalState terminal);
@@ -40,9 +34,7 @@ public interface ClientNetworkContext {
void handlePocketComputerDeleted(int instanceId); void handlePocketComputerDeleted(int instanceId);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume); void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
void handleSpeakerAudioPush(UUID source, ByteBuf buffer);
void handleSpeakerMove(UUID source, SpeakerPosition.Message position); void handleSpeakerMove(UUID source, SpeakerPosition.Message position);
@@ -51,18 +43,4 @@ public interface ClientNetworkContext {
void handleSpeakerStop(UUID source); void handleSpeakerStop(UUID source);
void handleUploadResult(int containerId, UploadResult result, @Nullable Component errorMessage); 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 dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import javax.annotation.Nullable;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.UUID; import java.util.UUID;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
/** /**
* Starts a sound on the client. * Starts a sound on the client.
* <p> * <p>
@@ -27,7 +24,7 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> { public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
private final UUID source; private final UUID source;
private final SpeakerPosition.Message pos; private final SpeakerPosition.Message pos;
private final @Nullable ByteBuffer content; private final ByteBuffer content;
private final float volume; private final float volume;
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) { 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); pos = SpeakerPosition.Message.read(buf);
volume = buf.readFloat(); volume = buf.readFloat();
// TODO: Remove this, so we no longer need a getter for ClientNetworkContext. However, doing so without var bytes = new byte[buf.readableBytes()];
// leaking or redundantly copying the buffer is hard. buf.readBytes(bytes);
ClientNetworkContext.get().handleSpeakerAudioPush(source, buf); content = ByteBuffer.wrap(bytes);
content = null;
} }
@Override @Override
@@ -53,12 +49,12 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
buf.writeUUID(source); buf.writeUUID(source);
pos.write(buf); pos.write(buf);
buf.writeFloat(volume); buf.writeFloat(volume);
buf.writeBytes(assertNonNull(content).duplicate()); buf.writeBytes(content.duplicate());
} }
@Override @Override
public void handle(ClientNetworkContext context) { public void handle(ClientNetworkContext context) {
context.handleSpeakerAudio(source, pos, volume); context.handleSpeakerAudio(source, pos, volume, content);
} }
@Override @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 com.google.errorprone.annotations.concurrent.GuardedBy;
import dan200.computercraft.api.filesystem.Mount; import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount; import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.common.AbstractContainerBlockEntity; import dan200.computercraft.shared.common.AbstractContainerBlockEntity;
import dan200.computercraft.shared.network.client.PlayRecordClientMessage; 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 dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
@@ -32,6 +33,28 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; 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 { public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
private static final String NBT_ITEM = "Item"; 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 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); 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; private MediaStack media = MediaStack.EMPTY;
@GuardedBy("this")
private @Nullable Mount mount; private @Nullable Mount mount;
private boolean recordPlaying = false; private boolean recordPlaying = false;
@@ -54,7 +79,12 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
// then read them when ticking. // then read them when ticking.
private final AtomicReference<RecordCommand> recordQueued = new AtomicReference<>(null); private final AtomicReference<RecordCommand> recordQueued = new AtomicReference<>(null);
private final AtomicBoolean ejectQueued = new AtomicBoolean(false); 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) { public DiskDriveBlockEntity(BlockEntityType<DiskDriveBlockEntity> type, BlockPos pos, BlockState state) {
super(type, pos, state); super(type, pos, state);
@@ -66,7 +96,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
@Override @Override
public void clearRemoved() { public void clearRemoved() {
updateItem(); updateMedia();
} }
@Override @Override
@@ -93,12 +123,14 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
} }
void serverTick() { void serverTick() {
if (stackDirty.getAndSet(false)) updateDiskFromMedia();
if (ejectQueued.getAndSet(false)) ejectContents(); if (ejectQueued.getAndSet(false)) ejectContents();
var recordQueued = this.recordQueued.getAndSet(null); var recordQueued = this.recordQueued.getAndSet(null);
if (recordQueued != null) { if (recordQueued != null) {
switch (recordQueued) { switch (recordQueued) {
case PLAY -> { case PLAY -> {
var media = getMedia();
var record = media.getAudio(); var record = media.getAudio();
if (record != null) { if (record != null) {
recordPlaying = true; recordPlaying = true;
@@ -112,12 +144,6 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
} }
} }
} }
if (mountQueued.get()) {
synchronized (this) {
mountAll();
}
}
} }
@Override @Override
@@ -127,25 +153,27 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
@Override @Override
public void setChanged() { public void setChanged() {
if (level != null && !level.isClientSide) updateItem(); if (level != null && !level.isClientSide) updateMedia();
super.setChanged(); super.setChanged();
} }
private void updateItem() { /**
var newDisk = getDiskStack(); * Called on the server after the item has changed. This unmounts the old media and mounts the new one.
if (ItemStack.isSameItemSameTags(newDisk, media.stack)) return; */
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); updateBlockState(DiskDriveState.EMPTY);
} else { } else {
updateBlockState(media.media != null ? DiskDriveState.FULL : DiskDriveState.INVALID); updateBlockState(newMedia.media() != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
} }
synchronized (this) {
// Unmount old disk // Unmount old disk
if (!this.media.stack.isEmpty()) { if (!media.stack().isEmpty()) {
for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue()); for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
} }
@@ -155,10 +183,16 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
recordPlaying = false; recordPlaying = false;
} }
// Use our new media, and (if needed) mount the new disk.
mount = null; mount = null;
this.media = media; media = newMedia;
stackDirty.set(false);
mountAll(); if (!newStack.isEmpty() && !computers.isEmpty()) {
var mount = getOrCreateMount(true);
for (var entry : computers.entrySet()) {
mountDisk(entry.getKey(), entry.getValue(), mount);
}
} }
} }
@@ -166,7 +200,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
return getItem(0); return getItem(0);
} }
MediaStack getMedia() { synchronized MediaStack getMedia() {
return media; return media;
} }
@@ -181,17 +215,32 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
} }
/** /**
* Update the current disk stack, assuming the underlying item does not change. Unlike * Update the inventory's disk stack from the media stack. Unlike {@link #setDiskStack(ItemStack)} this will not
* {@link #setDiskStack(ItemStack)} this will not change any mounts. * change any mounts.
*
* @param stack The new disk stack.
*/ */
void updateDiskStack(ItemStack stack) { private synchronized void updateDiskFromMedia() {
setItem(0, stack); // Write back the item to the main inventory, and then mark it as dirty.
if (!ItemStack.isSameItemSameTags(stack, media.stack)) { setItem(0, media.stack().copy());
media = MediaStack.of(stack);
super.setChanged(); 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);
}
} }
@Nullable @Nullable
@@ -212,7 +261,9 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
synchronized (this) { synchronized (this) {
var info = new MountInfo(); var info = new MountInfo();
computers.put(computer, info); computers.put(computer, info);
mountQueued.set(true); if (!media.stack().isEmpty()) {
mountDisk(computer, info, getOrCreateMount(level instanceof ServerLevel l && l.getServer().isSameThread()));
}
} }
} }
@@ -234,35 +285,31 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
ejectQueued.set(true); ejectQueued.set(true);
} }
/** synchronized MountResult setDiskLabel(@Nullable String label) {
* Add our mount to all computers. if (media.media() == null) return MountResult.NO_MEDIA;
*/
@GuardedBy("this") // Set the label, and write it back to the media stack.
private void mountAll() { var stack = media.stack().copy();
doMountAll(); if (!media.media().setLabel(stack, label)) return MountResult.NOT_ALLOWED;
mountQueued.set(false); 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") @GuardedBy("this")
private void doMountAll() { private @Nullable Mount getOrCreateMount(boolean immediate) {
if (computers.isEmpty() || media.media == null) return; if (media.media() == null) return null;
if (mount != null) return mount;
if (mount == null) { // Set the id (if needed) and write it back to the media stack.
var stack = getDiskStack(); var stack = media.stack().copy();
mount = media.media.createDataMount(stack, (ServerLevel) level); mount = media.media().createDataMount(stack, (ServerLevel) level);
setDiskStack(stack); updateMediaStack(stack, immediate);
return mount;
} }
if (mount == null) return; private static void mountDisk(IComputerAccess computer, MountInfo info, @Nullable Mount mount) {
for (var entry : computers.entrySet()) {
var computer = entry.getKey();
var info = entry.getValue();
if (info.mountPath != null) continue;
if (mount instanceof WritableMount writable) { if (mount instanceof WritableMount writable) {
// Try mounting at the lowest numbered "disk" name we can // Try mounting at the lowest numbered "disk" name we can
var n = 1; var n = 1;
@@ -270,18 +317,19 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable); info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
n++; n++;
} }
} else { } else if (mount != null) {
// Try mounting at the lowest numbered "disk" name we can // Try mounting at the lowest numbered "disk" name we can
var n = 1; var n = 1;
while (info.mountPath == null) { while (info.mountPath == null) {
info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount); info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount);
n++; n++;
} }
} else {
assert info.mountPath == null : "Mount path should be null";
} }
computer.queueEvent("disk", computer.getAttachmentName()); computer.queueEvent("disk", computer.getAttachmentName());
} }
}
private static void unmountDisk(IComputerAccess computer, MountInfo info) { private static void unmountDisk(IComputerAccess computer, MountInfo info) {
if (info.mountPath != null) { if (info.mountPath != null) {
@@ -315,7 +363,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
} }
private void sendMessage(PlayRecordClientMessage message) { 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 @Override
@@ -327,4 +375,10 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
PLAY, PLAY,
STOP, STOP,
} }
enum MountResult {
NO_MEDIA,
NOT_ALLOWED,
CHANGED,
}
} }

View File

@@ -51,7 +51,7 @@ public class DiskDrivePeripheral implements IPeripheral {
*/ */
@LuaFunction @LuaFunction
public final boolean isDiskPresent() { public final boolean isDiskPresent() {
return !diskDrive.getMedia().stack.isEmpty(); return !diskDrive.getMedia().stack().isEmpty();
} }
/** /**
@@ -64,7 +64,7 @@ public class DiskDrivePeripheral implements IPeripheral {
@LuaFunction @LuaFunction
public final Object[] getDiskLabel() { public final Object[] getDiskLabel() {
var media = diskDrive.getMedia(); 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) @LuaFunction(mainThread = true)
public final void setDiskLabel(Optional<String> label) throws LuaException { public final void setDiskLabel(Optional<String> label) throws LuaException {
var media = diskDrive.getMedia(); switch (diskDrive.setDiskLabel(label.map(StringUtil::normaliseLabel).orElse(null))) {
if (media.media == null) return; case NOT_ALLOWED -> throw new LuaException("Disk label cannot be changed");
case CHANGED, NO_MEDIA -> {
// 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");
} }
diskDrive.updateDiskStack(stack);
} }
/** /**
@@ -172,7 +168,7 @@ public class DiskDrivePeripheral implements IPeripheral {
@Nullable @Nullable
@LuaFunction @LuaFunction
public final Object[] getDiskID() { 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; 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. * 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); static final MediaStack EMPTY = new MediaStack(ItemStack.EMPTY, null);
final ItemStack stack; static MediaStack of(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) {
if (stack.isEmpty()) return EMPTY; if (stack.isEmpty()) return EMPTY;
var freshStack = stack.copy(); 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.BlockPos;
import net.minecraft.core.Direction; 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.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; 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. * @return The found component, or {@code null} if not present.
*/ */
@Nullable @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 dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; 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.entity.BlockEntity;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -43,7 +43,7 @@ public final class GenericPeripheralProvider<C extends Runnable> {
if (!lookups.contains(lookup)) lookups.add(lookup); 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); methods.forEachMethod(blockEntity, consumer);
for (var lookup : lookups) { for (var lookup : lookups) {
@@ -53,17 +53,11 @@ public final class GenericPeripheralProvider<C extends Runnable> {
} }
@Nullable @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; 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(); 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); return builder.toPeripheral(blockEntity, side);
} }
} }

View File

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

View File

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

View File

@@ -8,12 +8,10 @@ import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.platform.ComponentAccess; import dan200.computercraft.shared.platform.ComponentAccess;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag; import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -38,8 +36,8 @@ public final class WiredModemLocalPeripheral {
private @Nullable IPeripheral peripheral; private @Nullable IPeripheral peripheral;
private final ComponentAccess<IPeripheral> peripherals; private final ComponentAccess<IPeripheral> peripherals;
public WiredModemLocalPeripheral(Runnable invalidate) { public WiredModemLocalPeripheral(ComponentAccess<IPeripheral> peripherals) {
peripherals = PlatformHelper.get().createPeripheralAccess(x -> invalidate.run()); this.peripherals = peripherals;
} }
/** /**
@@ -126,7 +124,7 @@ public final class WiredModemLocalPeripheral {
if (world.getBlockState(offset).is(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE)) return null; 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; 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.computer.terminal.TerminalState;
import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.client.MonitorClientMessage; 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.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
@@ -40,7 +40,7 @@ public final class MonitorWatcher {
if (serverMonitor == null || monitor.enqueued) continue; if (serverMonitor == null || monitor.enqueued) continue;
var state = getState(monitor, serverMonitor); 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); var state = getState(tile, monitor);
PlatformHelper.get().sendToAllTracking(new MonitorClientMessage(pos, state), chunk); ServerNetworking.sendToAllTracking(new MonitorClientMessage(pos, state), chunk);
limit -= state.size(); limit -= state.size();
} }

View File

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

View File

@@ -15,14 +15,44 @@ import javax.annotation.Nullable;
import java.util.Optional; 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> * <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"> * <div class="recipe-container">
* <mc-recipe recipe="computercraft:printer"></mc-recipe> * <mc-recipe recipe="computercraft:printer"></mc-recipe>
* <mc-recipe recipe="computercraft:printed_pages"></mc-recipe>
* <mc-recipe recipe="computercraft:printed_book"></mc-recipe>
* </div> * </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.module printer
* @cc.see cc.strings.wrap To wrap text before printing it.
*/ */
public class PrinterPeripheral implements IPeripheral { public class PrinterPeripheral implements IPeripheral {
private final PrinterBlockEntity printer; 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.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.Nullability; import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; 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.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -32,7 +32,7 @@ public class SpeakerBlockEntity extends BlockEntity {
public void setRemoved() { public void setRemoved() {
super.setRemoved(); super.setRemoved();
if (level != null && !level.isClientSide) { 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.SpeakerMoveClientMessage;
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage; import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; 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 dan200.computercraft.shared.util.PauseAwareTimer;
import net.minecraft.ResourceLocationException; import net.minecraft.ResourceLocationException;
import net.minecraft.core.BlockPos; 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. // Stop the speaker and nuke the position, so we don't update it again.
if (shouldStop && lastPosition != null) { if (shouldStop && lastPosition != null) {
lastPosition = null; lastPosition = null;
PlatformHelper.get().sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server); ServerNetworking.sendToAllPlayers(new SpeakerStopClientMessage(getSource()), server);
return; return;
} }
var now = PauseAwareTimer.getTime(); var now = PauseAwareTimer.getTime();
if (sound != null) { if (sound != null) {
lastPlayTime = clock; lastPlayTime = clock;
PlatformHelper.get().sendToAllAround( ServerNetworking.sendToAllAround(
new SpeakerPlayClientMessage(getSource(), position, sound.sound, sound.volume, sound.pitch), new SpeakerPlayClientMessage(getSource(), position, sound.sound, sound.volume, sound.pitch),
(ServerLevel) level, pos, sound.volume * 16 (ServerLevel) level, pos, sound.volume * 16
); );
@@ -131,7 +131,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
} else if (dfpwmState != null && dfpwmState.shouldSendPending(now)) { } 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 // If clients need to receive another batch of audio, send it and then notify computers our internal buffer is
// free again. // free again.
PlatformHelper.get().sendToAllTracking( ServerNetworking.sendToAllTracking(
new SpeakerAudioClientMessage(getSource(), position, dfpwmState.getVolume(), dfpwmState.pullPending(now)), new SpeakerAudioClientMessage(getSource(), position, dfpwmState.getVolume(), dfpwmState.pullPending(now)),
level.getChunkAt(BlockPos.containing(pos)) level.getChunkAt(BlockPos.containing(pos))
); );
@@ -150,7 +150,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
// in the last second. // in the last second.
if (lastPosition != null && (clock - lastPositionTime) >= 20 && !lastPosition.withinDistance(position, 0.1)) { 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. // 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), new SpeakerMoveClientMessage(getSource(), position),
level.getChunkAt(BlockPos.containing(pos)) 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.api.peripheral.IComputerAccess;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; 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(); var server = level.getServer();
if (server == null || server.isStopped()) return; 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; package dan200.computercraft.shared.platform;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -18,15 +16,11 @@ import javax.annotation.Nullable;
public interface ComponentAccess<T> { public interface ComponentAccess<T> {
/** /**
* Get a peripheral for the current block. * 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. * @param direction The direction the peripheral is in.
* @return The peripheral, or {@literal null} if not found. * @return The peripheral, or {@literal null} if not found.
* @throws IllegalStateException If the level or position have changed. * @throws IllegalStateException If the level or position have changed.
*/ */
@Nullable @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.Direction;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.network.FriendlyByteBuf; 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.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode; 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.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; 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.BlockHitResult;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer; 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); <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 message The messsge to convert.
* @param player The player to send it to. * @return The converted message.
*/ */
void sendToPlayer(NetworkMessage<ClientNetworkContext> message, ServerPlayer player); Packet<ClientGamePacketListener> createPacket(NetworkMessage<ClientNetworkContext> message);
/**
* 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);
/** /**
* Create a {@link ComponentAccess} for surrounding peripherals. * 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> * @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. * include all changes, and so block updates should still be listened to.
* @return The peripheral component access. * @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. * 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> * @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. * include all changes, and so block updates should still be listened to.
* @return The peripheral component access. * @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 * 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.config.Config;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage; import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage; 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 dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
@@ -161,7 +161,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
if (sendState) { if (sendState) {
// Broadcast the state to all players // Broadcast the state to all players
tracking.addAll(getLevel().players()); tracking.addAll(getLevel().players());
PlatformHelper.get().sendToPlayers(new PocketComputerDataMessage(this, false), tracking); ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
} else { } else {
// Broadcast the state to new players. // Broadcast the state to new players.
List<ServerPlayer> added = new ArrayList<>(); List<ServerPlayer> added = new ArrayList<>();
@@ -169,7 +169,7 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
if (tracking.add(player)) added.add(player); if (tracking.add(player)) added.add(player);
} }
if (!added.isEmpty()) { 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()) { if (entity instanceof ServerPlayer player && entity.isAlive()) {
// Broadcast the terminal to the current player. // Broadcast the terminal to the current player.
PlatformHelper.get().sendToPlayer(new PocketComputerDataMessage(this, true), player); ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), player);
} }
} }
@Override @Override
protected void onRemoved() { protected void onRemoved() {
super.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 javax.annotation.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR; import static dan200.computercraft.shared.common.IColouredItem.NBT_COLOUR;
import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED; import static dan200.computercraft.shared.util.WaterloggableHelpers.WATERLOGGED;
@@ -59,8 +58,6 @@ public class TurtleBrain implements TurtleAccessInternal {
private static final int ANIM_DURATION = 8; 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 TurtleBlockEntity owner;
private @Nullable GameProfile owningPlayer; private @Nullable GameProfile owningPlayer;
@@ -694,7 +691,7 @@ public class TurtleBrain implements TurtleAccessInternal {
} }
var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ); 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()) { if (!list.isEmpty()) {
double pushStep = 1.0f / ANIM_DURATION; double pushStep = 1.0f / ANIM_DURATION;
var pushStepX = moveDir.getStepX() * pushStep; 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) { private float getAnimationFraction(float f) {
var next = (float) animationProgress / ANIM_DURATION; var next = (float) animationProgress / ANIM_DURATION;
var previous = (float) lastAnimationProgress / ANIM_DURATION; var previous = (float) lastAnimationProgress / ANIM_DURATION;

View File

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

View File

@@ -27,12 +27,8 @@ import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.phys.shapes.VoxelShape;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.Predicate;
public final class WorldUtil { 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) { public static boolean isLiquidBlock(Level world, BlockPos pos) {
if (!world.isInWorldBounds(pos)) return false; if (!world.isInWorldBounds(pos)) return false;
return world.getBlockState(pos).liquid(); return world.getBlockState(pos).liquid();
@@ -84,7 +80,7 @@ public final class WorldUtil {
Entity bestEntity = null; Entity bestEntity = null;
Vec3 bestHit = 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()); 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. // 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); 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) { public static Vec3 getRayStart(Player entity) {
return entity.getEyePosition(); 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.client.ClientNetworkContext;
import dan200.computercraft.shared.network.container.ContainerData; import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.platform.*; import dan200.computercraft.shared.platform.*;
import io.netty.buffer.Unpooled;
import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.FriendlyByteBuf; 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.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey; 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.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState; 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.BlockHitResult;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@@ -129,31 +130,6 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
throw new UnsupportedOperationException("Cannot register ArgumentTypeInfo inside tests"); 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 @Override
public List<TagKey<Item>> getDyeTags() { public List<TagKey<Item>> getDyeTags() {
throw new UnsupportedOperationException("Cannot query tags inside tests"); 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"); throw new UnsupportedOperationException("Cannot open menu inside tests");
} }
@Override record TypeImpl<T extends NetworkMessage<?>>(
public <T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) { ResourceLocation id, Function<FriendlyByteBuf, T> reader
record TypeImpl<T extends NetworkMessage<?>>(Function<FriendlyByteBuf, T> reader) implements MessageType<T> { ) implements MessageType<T> {
}
return new TypeImpl<>(reader);
} }
@Override @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"); throw new UnsupportedOperationException("Cannot interact with the world inside tests");
} }
@Override @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"); throw new UnsupportedOperationException("Cannot interact with the world inside tests");
} }

View File

@@ -4,9 +4,10 @@
package dan200.computercraft.client.sound; package dan200.computercraft.client.sound;
import io.netty.buffer.ByteBufAllocator;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
public class DfpwmStreamTest { public class DfpwmStreamTest {
@@ -14,8 +15,7 @@ public class DfpwmStreamTest {
public void testDecodesBytes() { public void testDecodesBytes() {
var stream = new DfpwmStream(); var stream = new DfpwmStream();
var input = ByteBufAllocator.DEFAULT.buffer(); 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 });
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 });
stream.push(input); stream.push(input);
var buffer = stream.read(1024 + 1); var buffer = stream.read(1024 + 1);

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.gametest package dan200.computercraft.gametest
import dan200.computercraft.core.apis.FSAPI import dan200.computercraft.core.apis.FSAPI
import dan200.computercraft.core.util.Colour
import dan200.computercraft.gametest.api.* import dan200.computercraft.gametest.api.*
import dan200.computercraft.shared.ModRegistry import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.media.items.DiskItem 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.ItemStack
import net.minecraft.world.item.Items import net.minecraft.world.item.Items
import net.minecraft.world.level.block.RedStoneWireBlock 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 import org.junit.jupiter.api.Assertions.assertEquals
class Disk_Drive_Test { class Disk_Drive_Test {
@@ -45,14 +49,48 @@ class Disk_Drive_Test {
thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) } 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. * A mount is initially attached, and then removed when the disk is ejected.
*/ */
@GameTest @GameTest
fun Adds_removes_mount(helper: GameTestHelper) = helper.sequence { fun Adds_removes_mount(helper: GameTestHelper) = helper.sequence {
thenOnComputer { } // Wait for the computer to start up thenOnComputer { } // Wait for the computer to start up
thenIdle(2) // Let the disk drive tick once to create the mount thenExecute {
thenOnComputer { // Then actually assert things! helper.setContainerItem(BlockPos(1, 2, 2), 0, DiskItem.createFromIDAndColour(1, null, Colour.BLACK.hex))
}
thenOnComputer {
getApi<FSAPI>().getDrive("disk").assertArrayEquals("right") getApi<FSAPI>().getDrive("disk").assertArrayEquals("right")
callPeripheral("right", "ejectDisk") callPeripheral("right", "ejectDisk")
} }

View File

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

View File

@@ -7,7 +7,10 @@ package dan200.computercraft.gametest
import dan200.computercraft.api.lua.ObjectArguments import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.core.computer.ComputerSide 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.ModRegistry
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
import dan200.computercraft.test.core.assertArrayEquals 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.item.ItemStack
import net.minecraft.world.level.block.Blocks import net.minecraft.world.level.block.Blocks
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
import java.util.*
class Monitor_Test { class Monitor_Test {
@GameTest @GameTest

View File

@@ -19,7 +19,6 @@ import net.minecraft.world.inventory.MenuType
import net.minecraft.world.inventory.TransientCraftingContainer import net.minecraft.world.inventory.TransientCraftingContainer
import net.minecraft.world.item.ItemStack import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items import net.minecraft.world.item.Items
import net.minecraft.world.item.crafting.CraftingRecipe
import net.minecraft.world.item.crafting.RecipeType import net.minecraft.world.item.crafting.RecipeType
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import java.util.* import java.util.*
@@ -37,11 +36,11 @@ class Recipe_Test {
container.setItem(0, ItemStack(Items.SKELETON_SKULL)) container.setItem(0, ItemStack(Items.SKELETON_SKULL))
container.setItem(1, ItemStack(ModRegistry.Items.COMPUTER_ADVANCED.get())) 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) .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") val profile = GameProfile(UUID.fromString("f3c8d69b-0776-4512-8434-d1b2165909eb"), "dan200")

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.gametest.api package dan200.computercraft.gametest.api
import dan200.computercraft.api.peripheral.IPeripheral
import dan200.computercraft.gametest.core.ManagedComputers import dan200.computercraft.gametest.core.ManagedComputers
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor 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.Entity
import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.EntityType
import net.minecraft.world.item.ItemStack 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.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState 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. * 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. * first `n` slots - the remaining are required to be empty.
*/ */
fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>) = fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>) =
when (val container = getBlockEntity(pos) ?: failVerbose("Expected a container at $pos, found nothing", pos)) { assertContainerExactlyImpl(pos, getContainerAt(pos), items)
is Container -> assertContainerExactlyImpl(pos, container, items)
else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
}
/** /**
* Assert an container contains exactly these items and no more. * 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) { fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) {
val peripheral = PlatformHelper.get().createPeripheralAccess { } val peripheral = getPeripheralAt(pos, direction)
.get(level, absolutePos(pos).relative(direction), direction.opposite)
when { when {
peripheral == null -> fail("No peripheral at position", pos) peripheral == null -> fail("No peripheral at position", pos)
peripheral.type != type -> fail("Peripheral is of type ${peripheral.type}, expected $type", 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) { fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) {
val peripheral = PlatformHelper.get().createPeripheralAccess { } val peripheral = getPeripheralAt(pos, direction)
.get(level, absolutePos(pos).relative(direction), direction.opposite)
if (peripheral != null) fail("Expected no peripheral, got a ${peripheral.type}", pos) 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) { fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
setBlock(pos, modify(getBlockState(pos))) 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: [0, 1, 4], state: "minecraft:air"},
{pos: [1, 1, 0], state: "minecraft:air"}, {pos: [1, 1, 0], state: "minecraft:air"},
{pos: [1, 1, 1], 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, 3], state: "minecraft:air"},
{pos: [1, 1, 4], state: "minecraft:air"}, {pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], 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()) 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) { val checkChangelog by tasks.registering(cc.tweaked.gradle.CheckChangelog::class) {
version.set(modVersion) version.set(modVersion)
whatsNew.set(file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md")) 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. * 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 * @param countArg The number of bytes to read. This may be 0 to determine we are at the end of the file. When
* may be 0 to determine we are at the end of the file. * absent, a single byte will be read.
* @return The read bytes. * @return The read bytes.
* @throws LuaException When trying to read a negative number of bytes. * @throws LuaException When trying to read a negative number of bytes.
* @throws LuaException If the file has been closed. * @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(); var allocated = ThreadAllocations.getAllocatedBytes(current) - info.allocatedBytes();
if (allocated > 0) { if (allocated > 0) {
metrics.observe(Metrics.JAVA_ALLOCATION, allocated); metrics.observe(Metrics.JAVA_ALLOCATION, allocated);
} else { } else if (allocated < 0) {
LOG.warn("Allocated a negative number of bytes!"); 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) { private LuaTable wrapLuaObject(Object object) {
var table = new LuaTable(); var table = new LuaTable();
var found = luaMethods.forEachMethod(object, (target, name, method, info) -> var found = luaMethods.forEachMethod(object, (target, name, method, info) ->
table.rawset(name, info != null && info.nonYielding() table.rawset(name, new ResultInterpreterFunction(this, method, target, context, name)));
? new BasicFunction(this, method, target, context, name)
: new ResultInterpreterFunction(this, method, target, context, name)));
return found ? table : null; return found ? table : null;
} }

View File

@@ -73,7 +73,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
} }
@Override @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; MethodResult results;
var arguments = CobaltLuaMachine.toObjects(args); var arguments = CobaltLuaMachine.toObjects(args);
try { try {
@@ -98,6 +98,6 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
if (!exception.hasLevel() && adjust == 0) return new LuaError(exception.getMessage()); if (!exception.hasLevel() && adjust == 0) return new LuaError(exception.getMessage());
var level = exception.getLevel(); 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 # New features in CC: Tweaked 1.109.3
* Command computers now display in the operator items creative tab. * 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. * 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 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 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 # 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: Several bug fixes:
* Error if too many websocket messages are queued to be sent at once. * Discard characters being typed into the editor when closing `edit`'s `Run` screen.
* 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.
Type "help changelog" to see the full version history. 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]) term.write(wrapped[i])
end end
os.pullEvent('key') os.pullEvent('key')
require "cc.internal.event".discard_char()
]] ]]
-- Menus -- Menus

View File

@@ -300,7 +300,7 @@ local menu_choices = {
return false return false
end, end,
Exit = function() 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 return true
end, end,
} }

View File

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

View File

@@ -67,8 +67,8 @@ public class MethodTest {
public void testPeripheralThrow() { public void testPeripheralThrow() {
ComputerBootstrap.run( ComputerBootstrap.run(
"local throw = peripheral.wrap('top')\n" + "local throw = peripheral.wrap('top')\n" +
"local _, err = pcall(throw.thisThread) assert(err == 'pcall: !', err)\n" + "local _, err = pcall(function() throw.thisThread() end) assert(err == '/test.lua:2: !', (\"thisThread: %q\"):format(err))\n" +
"local _, err = pcall(throw.mainThread) assert(err == 'pcall: !', err)", "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()), x -> x.getEnvironment().setPeripheral(ComputerSide.TOP, new PeripheralThrow()),
50 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("testWithSodium")
addRemappedConfiguration("testWithIris") 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 { dependencies {
clientCompileOnly(variantOf(libs.emi) { classifier("api") }) clientCompileOnly(variantOf(libs.emi) { classifier("api") })
modImplementation(libs.bundles.externalMods.fabric) { cct.exclude(this) }
modCompileOnly(libs.bundles.externalMods.fabric.compile) { modCompileOnly(libs.bundles.externalMods.fabric.compile) {
exclude("net.fabricmc", "fabric-loader") exclude("net.fabricmc", "fabric-loader")
exclude("net.fabricmc.fabric-api") exclude("net.fabricmc.fabric-api")
@@ -63,24 +79,19 @@ dependencies {
"modTestWithIris"(libs.iris) "modTestWithIris"(libs.iris)
"modTestWithIris"(libs.sodium) "modTestWithIris"(libs.sodium)
include(libs.cobalt) "includeRuntimeOnly"(libs.cobalt)
include(libs.jzlib) "includeRuntimeOnly"(libs.jzlib)
include(libs.netty.http) "includeRuntimeOnly"(libs.netty.http)
include(libs.netty.socks) "includeRuntimeOnly"(libs.netty.socks)
include(libs.netty.proxy) "includeRuntimeOnly"(libs.netty.proxy)
include(libs.nightConfig.core)
include(libs.nightConfig.toml) "includeImplementation"(libs.nightConfig.core)
"includeImplementation"(libs.nightConfig.toml)
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness. // Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
api(commonClasses(project(":fabric-api"))) { cct.exclude(this) } api(commonClasses(project(":fabric-api"))) { cct.exclude(this) }
clientApi(clientClasses(project(":fabric-api"))) { cct.exclude(this) } clientApi(clientClasses(project(":fabric-api"))) { cct.exclude(this) }
implementation(project(":core")) { 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) annotationProcessorEverywhere(libs.autoService)
@@ -135,7 +146,6 @@ loom {
client() client()
runDir("run/dataGen") runDir("run/dataGen")
property("cct.pretty-json")
property("fabric-api.datagen") property("fabric-api.datagen")
property("fabric-api.datagen.output-dir", file("src/generated/resources").absolutePath) property("fabric-api.datagen.output-dir", file("src/generated/resources").absolutePath)
property("fabric-api.datagen.strict-validation") property("fabric-api.datagen.strict-validation")

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.client; package dan200.computercraft.client;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.FabricComputerCraftAPIClient;
import dan200.computercraft.client.model.CustomModelLoader; import dan200.computercraft.client.model.CustomModelLoader;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.config.ConfigSpec; import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.NetworkMessages; 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.FabricConfigFile;
import dan200.computercraft.shared.platform.FabricMessageType; import dan200.computercraft.shared.platform.FabricMessageType;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap; 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.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin; import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 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 class ComputerCraftClient {
public static void init() { public static void init() {
var clientNetwork = Services.load(ClientNetworkContext.class);
for (var type : NetworkMessages.getClientbound()) { for (var type : NetworkMessages.getClientbound()) {
ClientPlayNetworking.registerGlobalReceiver( 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.register();
ClientRegistry.registerTurtleModellers(FabricComputerCraftAPIClient::registerTurtleUpgradeModeller);
ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register); ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register);
ClientRegistry.registerMainThread(); ClientRegistry.registerMainThread();
@@ -77,6 +83,10 @@ public class ComputerCraftClient {
return cable.getCloneItemStack(state, hit, level, pos, player); 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")); ((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.network.server.ServerNetworkContext;
import dan200.computercraft.shared.platform.FabricMessageType; import dan200.computercraft.shared.platform.FabricMessageType;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 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.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.Sheets; import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.entity.ItemRenderer; import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ServerGamePacketListener;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.util.RandomSource; import net.minecraft.util.RandomSource;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -28,8 +34,10 @@ public class ClientPlatformHelperImpl implements ClientPlatformHelper {
private static final RandomSource random = RandomSource.create(0); private static final RandomSource random = RandomSource.create(0);
@Override @Override
public void sendToServer(NetworkMessage<ServerNetworkContext> message) { public Packet<ServerGamePacketListener> createPacket(NetworkMessage<ServerNetworkContext> message) {
ClientPlayNetworking.send(FabricMessageType.toFabricPacket(message)); var buf = PacketByteBufs.create();
message.write(buf);
return ClientPlayNetworking.createC2SPacket(FabricMessageType.toFabricType(message.type()).getId(), buf);
} }
@Override @Override
@@ -55,4 +63,9 @@ public class ClientPlatformHelperImpl implements ClientPlatformHelper {
ModelRenderer.renderQuads(transform, buffer, model.getQuads(null, face, random), lightmapCoord, overlayLight, tints); 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": "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_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": "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": "Proxy",
"gui.computercraft.config.http.proxy.host": "Host name", "gui.computercraft.config.http.proxy.host": "Host name",
"gui.computercraft.config.http.proxy.host.tooltip": "The hostname or IP address of the proxy server.", "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 { public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override @Override
public void onInitializeDataGenerator(FabricDataGenerator generator) { public void onInitializeDataGenerator(FabricDataGenerator generator) {
var pack = generator.createPack(); var pack = new PlatformGeneratorsImpl(generator.createPack());
DataProviders.add(new PlatformGeneratorsImpl(pack)); DataProviders.add(pack);
pack.addProvider((out, reg) -> addName("Conventional Tags", new MoreConventionalTagsProvider(out, reg))); pack.addWithRegistries((out, reg) -> addName("Conventional Tags", new MoreConventionalTagsProvider(out, reg)));
} }
private record PlatformGeneratorsImpl(FabricDataGenerator.Pack generator) implements DataProviders.GeneratorSink { 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 @Override
public <T extends DataProvider> T add(DataProvider.Factory<T> factory) { 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 @Override
public <T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output) { 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) { var ourType = switch (type) {
case SERVER_DATA -> PackOutput.Target.DATA_PACK; case SERVER_DATA -> PackOutput.Target.DATA_PACK;
case CLIENT_RESOURCES -> PackOutput.Target.RESOURCE_PACK; case CLIENT_RESOURCES -> PackOutput.Target.RESOURCE_PACK;
@@ -71,7 +79,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override @Override
public void lootTable(List<LootTableProvider.SubProviderEntry> tables) { public void lootTable(List<LootTableProvider.SubProviderEntry> tables) {
for (var table : tables) { for (var table : tables) {
generator.addProvider((FabricDataOutput out) -> new SimpleFabricLootTableProvider(out, table.paramSet()) { addWithFabricOutput((FabricDataOutput out) -> new SimpleFabricLootTableProvider(out, table.paramSet()) {
@Override @Override
public void generate(BiConsumer<ResourceLocation, LootTable.Builder> exporter) { public void generate(BiConsumer<ResourceLocation, LootTable.Builder> exporter) {
table.provider().get().generate(exporter); table.provider().get().generate(exporter);
@@ -82,7 +90,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override @Override
public TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags) { 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 @Override
protected void addTags(HolderLookup.Provider registries) { protected void addTags(HolderLookup.Provider registries) {
tags.accept(x -> new TagProvider.TagAppender<>(RegistryWrappers.BLOCKS, getOrCreateRawBuilder(x))); tags.accept(x -> new TagProvider.TagAppender<>(RegistryWrappers.BLOCKS, getOrCreateRawBuilder(x)));
@@ -92,7 +100,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override @Override
public TagsProvider<Item> itemTags(Consumer<TagProvider.ItemTagConsumer> tags, TagsProvider<Block> blocks) { 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 @Override
protected void addTags(HolderLookup.Provider registries) { protected void addTags(HolderLookup.Provider registries) {
var self = this; var self = this;
@@ -113,7 +121,7 @@ public class FabricDataGenerators implements DataGeneratorEntrypoint {
@Override @Override
public void models(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) { public void models(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) {
generator.addProvider((FabricDataOutput out) -> new FabricModelProvider(out) { addWithFabricOutput((FabricDataOutput out) -> new FabricModelProvider(out) {
@Override @Override
public void generateBlockStateModels(BlockModelGenerators generator) { public void generateBlockStateModels(BlockModelGenerators generator) {
blocks.accept(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 dan200.computercraft.shared.peripheral.generic.GenericPeripheralProvider;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; 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.entity.BlockEntity;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -29,7 +29,7 @@ public final class Peripherals {
genericProvider.registerLookup(lookup); 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); 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.details.FluidDetails;
import dan200.computercraft.shared.network.NetworkMessages; import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.network.client.UpgradesLoadedMessage; 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.commandblock.CommandBlockPeripheral;
import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods; import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlockEntity; 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.peripheral.modem.wireless.WirelessModemBlockEntity;
import dan200.computercraft.shared.platform.FabricConfigFile; import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.platform.FabricMessageType; 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.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
@@ -92,7 +92,7 @@ public class ComputerCraft {
CommonHooks.onServerStopped(); CommonHooks.onServerStopped();
((FabricConfigFile) ConfigSpec.serverSpec).unload(); ((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(CommonHooks::onServerTickStart);
ServerTickEvents.START_SERVER_TICK.register(s -> CommonHooks.onServerTickEnd()); ServerTickEvents.START_SERVER_TICK.register(s -> CommonHooks.onServerTickEnd());

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