mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 13:42:59 +00:00 
			
		
		
		
	Merge branch 'mc-1.20.x' into mc-1.21.x
This commit is contained in:
		| @@ -27,7 +27,7 @@ repos: | ||||
|     exclude: "^(.*\\.(bat)|LICENSE)$" | ||||
|  | ||||
| - repo: https://github.com/fsfe/reuse-tool | ||||
|   rev: v2.1.0 | ||||
|   rev: v4.0.3 | ||||
|   hooks: | ||||
|   - id: reuse | ||||
|  | ||||
|   | ||||
							
								
								
									
										100
									
								
								.reuse/dep5
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								.reuse/dep5
									
									
									
									
									
								
							| @@ -1,100 +0,0 @@ | ||||
| Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ | ||||
| Source: https://github.com/cc-tweaked/cc-tweaked | ||||
| Upstream-Name: CC: Tweaked | ||||
| Upstream-Contact: Jonathan Coates <git@squiddev.cc> | ||||
|  | ||||
| Files: | ||||
|   projects/common/src/main/resources/assets/computercraft/sounds.json | ||||
|   projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg | ||||
|   projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrade/* | ||||
|   projects/common/src/testMod/resources/data/cctest/structures/* | ||||
|   projects/*/src/generated/* | ||||
|   projects/web/src/htmlTransform/export/index.json | ||||
|   projects/web/src/htmlTransform/export/items/minecraft/* | ||||
| Comment: Generated/data files are CC0. | ||||
| Copyright: The CC: Tweaked Developers | ||||
| License: CC0-1.0 | ||||
|  | ||||
| Files: | ||||
|   doc/images/* | ||||
|   package.json | ||||
|   package-lock.json | ||||
|   projects/common/src/client/resources/computercraft-client.mixins.json | ||||
|   projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.json | ||||
|   projects/common/src/main/resources/computercraft.mixins.json | ||||
|   projects/common/src/testMod/resources/computercraft-gametest.mixins.json | ||||
|   projects/common/src/testMod/resources/data/computercraft/loot_tables/treasure_disk.json | ||||
|   projects/common/src/testMod/resources/pack.mcmeta | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/modules/command/.ignoreme | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/.ignoreme | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/modules/turtle/.ignoreme | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/motd.txt | ||||
|   projects/fabric-api/src/main/modJson/fabric.mod.json | ||||
|   projects/fabric/src/client/resources/computercraft-client.fabric.mixins.json | ||||
|   projects/fabric/src/main/resources/computercraft.fabric.mixins.json | ||||
|   projects/fabric/src/main/resources/fabric.mod.json | ||||
|   projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json | ||||
|   projects/fabric/src/testMod/resources/fabric.mod.json | ||||
|   projects/forge/src/client/resources/computercraft-client.forge.mixins.json | ||||
|   projects/forge/src/main/resources/computercraft.forge.mixins.json | ||||
|   projects/web/src/frontend/mount/.settings | ||||
|   projects/web/src/frontend/mount/example.nfp | ||||
|   projects/web/src/frontend/mount/example.nft | ||||
|   projects/web/src/frontend/mount/expr_template.lua | ||||
|   projects/web/tsconfig.json | ||||
| Comment: Several assets where it's inconvenient to create a .license file. | ||||
| Copyright: The CC: Tweaked Developers | ||||
| License: MPL-2.0 | ||||
|  | ||||
| Files: | ||||
|   doc/logo.png | ||||
|   doc/logo-darkmode.png | ||||
|   projects/common/src/main/resources/assets/computercraft/models/* | ||||
|   projects/common/src/main/resources/assets/computercraft/textures/* | ||||
|   projects/common/src/main/resources/pack.mcmeta | ||||
|   projects/common/src/main/resources/pack.png | ||||
|   projects/core/src/main/resources/assets/computercraft/textures/gui/term_font.png | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/autorun/.ignoreme | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/help/* | ||||
|   projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/levels/* | ||||
|   projects/web/src/htmlTransform/export/items/computercraft/* | ||||
| Comment: Bulk-license original assets as CCPL. | ||||
| Copyright: 2011 Daniel Ratcliffe | ||||
| License: LicenseRef-CCPL | ||||
|  | ||||
| Files: | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/cs_cz.json | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/ko_kr.json | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/pl_pl.json | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/pt_br.json | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/uk_ua.json | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/zh_cn.json | ||||
| Comment: Community-contributed license files | ||||
| Copyright: 2017 The CC: Tweaked Developers | ||||
| License: LicenseRef-CCPL | ||||
|  | ||||
| Files: | ||||
|   projects/common/src/main/resources/assets/computercraft/lang/* | ||||
| Comment: Community-contributed license files | ||||
| Copyright: 2017 The CC: Tweaked Developers | ||||
| License: MPL-2.0 | ||||
|  | ||||
| Files: | ||||
|   .github/* | ||||
| Comment: | ||||
|   GitHub build scripts are CC0. While we could add a header to each file, | ||||
|   it's unclear if it will break actions or issue templates in some way. | ||||
| Copyright: Jonathan Coates <git@squiddev.cc> | ||||
| License: CC0-1.0 | ||||
|  | ||||
| Files: | ||||
|   gradle/wrapper/* | ||||
|   gradlew | ||||
|   gradlew.bat | ||||
| Copyright: Gradle Inc | ||||
| License: Apache-2.0 | ||||
|  | ||||
| Files: projects/core/src/test/resources/test-rom/data/json-parsing/* | ||||
| Copyright: 2016 Nicolas Seriot | ||||
| License: MIT | ||||
| @@ -12,7 +12,6 @@ If you've any other questions, [just ask the community][community] or [open an i | ||||
| 
 | ||||
| ## Table of Contents | ||||
|  - [Reporting issues](#reporting-issues) | ||||
|  - [Translations](#translations) | ||||
|  - [Setting up a development environment](#setting-up-a-development-environment) | ||||
|  - [Developing CC: Tweaked](#developing-cc-tweaked) | ||||
|  - [Writing documentation](#writing-documentation) | ||||
| @@ -21,17 +20,13 @@ If you've any other questions, [just ask the community][community] or [open an i | ||||
| If you have a bug, suggestion, or other feedback, the best thing to do is [file an issue][new-issue]. When doing so, do | ||||
| use the issue templates - they provide a useful hint on what information to provide. | ||||
| 
 | ||||
| ## Translations | ||||
| Translations are managed through [Weblate], an online interface for managing language strings. This is synced | ||||
| automatically with GitHub, so please don't submit PRs adding/changing translations! | ||||
| 
 | ||||
| ## Setting up a development environment | ||||
| In order to develop CC: Tweaked, you'll need to download the source code and then run it. | ||||
| 
 | ||||
|  - Make sure you've got the following software installed: | ||||
|    - Java Development Kit (JDK). This can be downloaded from [Adoptium]. | ||||
|    - Java Development Kit 21 (JDK). This can be downloaded from [Adoptium]. | ||||
|    - [Git](https://git-scm.com/). | ||||
|    - [NodeJS][node]. | ||||
|    - [NodeJS 20 or later][node]. | ||||
| 
 | ||||
|  - Download CC: Tweaked's source code: | ||||
|    ``` | ||||
| @@ -101,7 +96,6 @@ about how you can build on that until you've covered everything! | ||||
| [community]: README.md#community "Get in touch with the community." | ||||
| [Adoptium]: https://adoptium.net/temurin/releases?version=17 "Download OpenJDK 17" | ||||
| [illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub" | ||||
| [weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance" | ||||
| [docs]: https://tweaked.cc/ "CC: Tweaked documentation" | ||||
| [ldoc]: http://stevedonovan.github.io/ldoc/ "ldoc, a Lua documentation generator." | ||||
| [mc-test]: https://www.youtube.com/watch?v=vXaWOJTCYNg | ||||
|   | ||||
| @@ -26,8 +26,9 @@ developing the mod, [check out the instructions here](CONTRIBUTING.md#developing | ||||
| 
 | ||||
| ## Community | ||||
| If you need help getting started with CC: Tweaked, want to show off your latest project, or just want to chat about | ||||
| ComputerCraft, do check out our [forum] and [GitHub discussions page][GitHub discussions]! There's also a fairly | ||||
| populated, albeit quiet [IRC channel][irc], if that's more your cup of tea. | ||||
| ComputerCraft, do check out our [GitHub discussions page][GitHub discussions]! There's also a fairly populated, | ||||
| albeit quiet IRC channel on [EsperNet], if that's more your cup of tea. You can join `#computercraft` through your | ||||
| desktop client, or online using [KiwiIRC]. | ||||
| 
 | ||||
| We also host fairly comprehensive documentation at [tweaked.cc](https://tweaked.cc/ "The CC: Tweaked website"). | ||||
| 
 | ||||
| @@ -85,6 +86,6 @@ the generated documentation [can be browsed online](https://tweaked.cc/javadoc/) | ||||
| [modrinth]: https://modrinth.com/mod/gu7yAYhd "Download CC: Tweaked from Modrinth" | ||||
| [Minecraft Forge]: https://files.minecraftforge.net/ "Download Minecraft Forge." | ||||
| [Fabric]: https://fabricmc.net/use/installer/ "Download Fabric." | ||||
| [forum]: https://forums.computercraft.cc/ | ||||
| [GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions | ||||
| [IRC]: https://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" | ||||
| [EsperNet]: https://www.esper.net/ | ||||
| [KiwiIRC]: https://kiwiirc.com/nextclient/#irc://irc.esper.net:+6697/#computercraft "#computercraft on EsperNet" | ||||
|   | ||||
							
								
								
									
										111
									
								
								REUSE.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								REUSE.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| # SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers | ||||
| # | ||||
| # SPDX-License-Identifier: MPL-2.0 | ||||
|  | ||||
| version = 1 | ||||
| SPDX-PackageName = "CC: Tweaked" | ||||
| SPDX-PackageSupplier = "Jonathan Coates <git@squiddev.cc>" | ||||
| SPDX-PackageDownloadLocation = "https://github.com/cc-tweaked/cc-tweaked" | ||||
|  | ||||
| [[annotations]] | ||||
| # Generated/data files are CC0. | ||||
| SPDX-FileCopyrightText = "The CC: Tweaked Developers" | ||||
| SPDX-License-Identifier = "CC0-1.0" | ||||
| path = [ | ||||
|     "gradle/gradle-daemon-jvm.properties", | ||||
|     "projects/common/src/main/resources/assets/computercraft/sounds.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg", | ||||
|     "projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrade/**", | ||||
|     "projects/common/src/testMod/resources/data/cctest/structures/**", | ||||
|     "projects/**/src/generated/**", | ||||
|     "projects/web/src/htmlTransform/export/index.json", | ||||
|     "projects/web/src/htmlTransform/export/items/minecraft/**", | ||||
| ] | ||||
|  | ||||
| [[annotations]] | ||||
| # Several assets where it's inconvenient to create a .license file. | ||||
| SPDX-FileCopyrightText = "The CC: Tweaked Developers" | ||||
| SPDX-License-Identifier = "MPL-2.0" | ||||
| path = [ | ||||
|     "doc/images/**", | ||||
|     "package.json", | ||||
|     "package-lock.json", | ||||
|     "projects/common/src/client/resources/computercraft-client.mixins.json", | ||||
|     "projects/common/src/main/resources/assets/minecraft/shaders/core/computercraft/monitor_tbo.json", | ||||
|     "projects/common/src/main/resources/computercraft.mixins.json", | ||||
|     "projects/common/src/testMod/resources/computercraft-gametest.mixins.json", | ||||
|     "projects/common/src/testMod/resources/data/computercraft/loot_tables/treasure_disk.json", | ||||
|     "projects/common/src/testMod/resources/pack.mcmeta", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/modules/command/.ignoreme", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/modules/main/.ignoreme", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/modules/turtle/.ignoreme", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/motd.txt", | ||||
|     "projects/fabric-api/src/main/modJson/fabric.mod.json", | ||||
|     "projects/fabric/src/client/resources/computercraft-client.fabric.mixins.json", | ||||
|     "projects/fabric/src/main/resources/computercraft.fabric.mixins.json", | ||||
|     "projects/fabric/src/main/resources/fabric.mod.json", | ||||
|     "projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json", | ||||
|     "projects/fabric/src/testMod/resources/fabric.mod.json", | ||||
|     "projects/forge/src/client/resources/computercraft-client.forge.mixins.json", | ||||
|     "projects/forge/src/main/resources/computercraft.forge.mixins.json", | ||||
|     "projects/web/src/frontend/mount/.settings", | ||||
|     "projects/web/src/frontend/mount/example.nfp", | ||||
|     "projects/web/src/frontend/mount/example.nft", | ||||
|     "projects/web/src/frontend/mount/expr_template.lua", | ||||
|     "projects/web/tsconfig.json", | ||||
| ] | ||||
|  | ||||
| [[annotations]] | ||||
| # Bulk-license original assets as CCPL. | ||||
| SPDX-FileCopyrightText = "2011 Daniel Ratcliffe" | ||||
| SPDX-License-Identifier = "LicenseRef-CCPL" | ||||
| path = [ | ||||
|     "doc/logo.png", | ||||
|     "doc/logo-darkmode.png", | ||||
|     "projects/common/src/main/resources/assets/computercraft/models/**", | ||||
|     "projects/common/src/main/resources/assets/computercraft/textures/**", | ||||
|     "projects/common/src/main/resources/pack.mcmeta", | ||||
|     "projects/common/src/main/resources/pack.png", | ||||
|     "projects/core/src/main/resources/assets/computercraft/textures/gui/term_font.png", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/autorun/.ignoreme", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/help/**", | ||||
|     "projects/core/src/main/resources/data/computercraft/lua/rom/programs/fun/advanced/levels/**", | ||||
|     "projects/web/src/htmlTransform/export/items/computercraft/**", | ||||
| ] | ||||
|  | ||||
| [[annotations]] | ||||
| # Community-contributed license files | ||||
| SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" | ||||
| SPDX-License-Identifier = "LicenseRef-CCPL" | ||||
| path = [ | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/cs_cz.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/ko_kr.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/pl_pl.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/pt_br.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/ru_ru.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/uk_ua.json", | ||||
|     "projects/common/src/main/resources/assets/computercraft/lang/zh_cn.json", | ||||
| ] | ||||
|  | ||||
| [[annotations]] | ||||
| # Community-contributed license files | ||||
| SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" | ||||
| SPDX-License-Identifier = "MPL-2.0" | ||||
| path = "projects/common/src/main/resources/assets/computercraft/lang/**" | ||||
|  | ||||
| [[annotations]] | ||||
| # GitHub build scripts are CC0. While we could add a header to each file, | ||||
| # it's unclear if it will break actions or issue templates in some way. | ||||
| SPDX-FileCopyrightText = "Jonathan Coates <git@squiddev.cc>" | ||||
| SPDX-License-Identifier = "CC0-1.0" | ||||
| path = ".github/**" | ||||
|  | ||||
| [[annotations]] | ||||
| path = ["gradle/wrapper/**", "gradlew", "gradlew.bat"] | ||||
| SPDX-FileCopyrightText = "Gradle Inc" | ||||
| SPDX-License-Identifier = "Apache-2.0" | ||||
|  | ||||
| [[annotations]] | ||||
| path = "projects/core/src/test/resources/test-rom/data/json-parsing/**" | ||||
| SPDX-FileCopyrightText = "2016 Nicolas Seriot" | ||||
| SPDX-License-Identifier = "MIT" | ||||
| @@ -47,6 +47,7 @@ repositories { | ||||
|         filter { | ||||
|             includeGroup("cc.tweaked") | ||||
|             // Things we mirror | ||||
|             includeGroup("com.simibubi.create") | ||||
|             includeGroup("commoble.morered") | ||||
|             includeGroup("dev.architectury") | ||||
|             includeGroup("dev.emi") | ||||
|   | ||||
| @@ -22,7 +22,6 @@ import org.gradle.api.tasks.SourceSet | ||||
| import org.gradle.api.tasks.bundling.Jar | ||||
| import org.gradle.api.tasks.compile.JavaCompile | ||||
| import org.gradle.api.tasks.javadoc.Javadoc | ||||
| import org.gradle.configurationcache.extensions.capitalized | ||||
| import org.gradle.language.base.plugins.LifecycleBasePlugin | ||||
| import org.gradle.language.jvm.tasks.ProcessResources | ||||
| import org.gradle.process.JavaForkOptions | ||||
| @@ -181,7 +180,7 @@ abstract class CCTweakedExtension( | ||||
| 
 | ||||
|     fun <T> jacoco(task: NamedDomainObjectProvider<T>) where T : Task, T : JavaForkOptions { | ||||
|         val classDump = project.layout.buildDirectory.dir("jacocoClassDump/${task.name}") | ||||
|         val reportTaskName = "jacoco${task.name.capitalized()}Report" | ||||
|         val reportTaskName = "jacoco${task.name.capitalise()}Report" | ||||
| 
 | ||||
|         val jacoco = project.extensions.getByType(JacocoPluginExtension::class.java) | ||||
|         task.configure { | ||||
|   | ||||
| @@ -171,3 +171,15 @@ fun getNextVersion(version: String): String { | ||||
|     if (dashIndex >= 0) out.append(version, dashIndex, version.length) | ||||
|     return out.toString() | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Capitalise the first letter of the string. | ||||
|  * | ||||
|  * This is a replacement for the now deprecated [String.capitalize]. | ||||
|  */ | ||||
| fun String.capitalise(): String { | ||||
|     if (isEmpty()) return this | ||||
|     val first = this[0] | ||||
|     val firstTitle = first.titlecaseChar() | ||||
|     return if (first == firstTitle) this else firstTitle + substring(1) | ||||
| } | ||||
|   | ||||
| @@ -191,7 +191,7 @@ end | ||||
| 
 | ||||
| > [Confused?][!NOTE] | ||||
| > Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't | ||||
| > cover. That said, don't be afraid to ask on [GitHub Discussions] or [IRC] either! | ||||
| > cover. That said, don't be afraid to ask [the community for help][community]. | ||||
| 
 | ||||
| It's worth noting that the examples of audio processing we've mentioned here are about manipulating the _amplitude_ of | ||||
| the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex. | ||||
| @@ -205,5 +205,4 @@ This is, I'm afraid, left as an exercise to the reader. | ||||
| [PCM]: https://en.wikipedia.org/wiki/Pulse-code_modulation "Pulse-code Modulation - Wikipedia" | ||||
| [Ring Buffer]: https://en.wikipedia.org/wiki/Circular_buffer "Circular buffer - Wikipedia" | ||||
| [Sine Wave]: https://en.wikipedia.org/wiki/Sine_wave "Sine wave - Wikipedia" | ||||
| [GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions | ||||
| [IRC]: https://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" | ||||
| [Community]: /#community | ||||
|   | ||||
| @@ -50,7 +50,11 @@ little daunting getting started. Thankfully, there's several fantastic tutorials | ||||
| Once you're a little more familiar with the mod, the sidebar and links below provide more detailed documentation on the | ||||
| various APIs and peripherals provided by the mod. | ||||
| 
 | ||||
| If you get stuck, do [ask a question on GitHub][GitHub Discussions] or pop in to the ComputerCraft's [IRC channel][IRC]. | ||||
| <h2 id="community">Community</h2> | ||||
| If you need help getting started with CC: Tweaked, want to show off your latest project, or just want to chat about | ||||
| ComputerCraft, do check out our [GitHub discussions page][GitHub discussions]! There's also a fairly populated, | ||||
| albeit quiet IRC channel on [EsperNet], if that's more your cup of tea. You can join `#computercraft` through your | ||||
| desktop client, or online using [KiwiIRC]. | ||||
| 
 | ||||
| ## Get Involved | ||||
| CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please do [create an issue][bug]. | ||||
| @@ -65,4 +69,5 @@ CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please | ||||
| [Fabric]: https://fabricmc.net/use/installer/ "Download Fabric." | ||||
| [lua]: https://www.lua.org/ "Lua's main website" | ||||
| [GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions | ||||
| [IRC]: https://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" | ||||
| [EsperNet]: https://www.esper.net/ | ||||
| [KiwiIRC]: https://kiwiirc.com/nextclient/#irc://irc.esper.net:+6697/#computercraft "#computercraft on EsperNet" | ||||
|   | ||||
| @@ -50,7 +50,11 @@ little daunting getting started. Thankfully, there's several fantastic tutorials | ||||
| Once you're a little more familiar with the mod, the [wiki](https://tweaked.cc/) provides more detailed documentation on the | ||||
| various APIs and peripherals provided by the mod. | ||||
| 
 | ||||
| If you get stuck, do [ask a question on GitHub][GitHub Discussions] or pop in to the ComputerCraft's [IRC channel][IRC]. | ||||
| ## Community | ||||
| If you need help getting started with CC: Tweaked, want to show off your latest project, or just want to chat about | ||||
| ComputerCraft, do check out our [GitHub discussions page][GitHub discussions]! There's also a fairly populated, | ||||
| albeit quiet IRC channel on [EsperNet], if that's more your cup of tea. You can join `#computercraft` through your | ||||
| desktop client, or online using [KiwiIRC]. | ||||
| 
 | ||||
| ## Get Involved | ||||
| CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please do [create an issue][bug]. | ||||
| @@ -60,4 +64,5 @@ CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please | ||||
| [computercraft]: https://github.com/dan200/ComputerCraft "ComputerCraft on GitHub" | ||||
| [lua]: https://www.lua.org/ "Lua's main website" | ||||
| [GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions | ||||
| [IRC]: http://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" | ||||
| [EsperNet]: https://www.esper.net/ | ||||
| [KiwiIRC]: https://kiwiirc.com/nextclient/#irc://irc.esper.net:+6697/#computercraft "#computercraft on EsperNet" | ||||
|   | ||||
| @@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false | ||||
|  | ||||
| # Mod properties | ||||
| isUnstable=true | ||||
| modVersion=1.111.1 | ||||
| modVersion=1.112.0 | ||||
|  | ||||
| # Minecraft properties: We want to configure this here so we can read it in settings.gradle | ||||
| mcVersion=1.21 | ||||
|   | ||||
							
								
								
									
										2
									
								
								gradle/gradle-daemon-jvm.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								gradle/gradle-daemon-jvm.properties
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| #This file is generated by updateDaemonJvm | ||||
| toolchainVersion=21 | ||||
| @@ -47,6 +47,8 @@ rei = "16.0.729" | ||||
| rubidium = "0.6.1" | ||||
| sodium = "mc1.20-0.4.10" | ||||
| mixinExtra = "0.3.5" | ||||
| create-forge = "0.5.1.f-33" | ||||
| create-fabric = "0.5.1-f-build.1467+mc1.20.1" | ||||
|  | ||||
| # Testing | ||||
| hamcrest = "2.2" | ||||
| @@ -100,11 +102,13 @@ nightConfig-toml = { module = "com.electronwill.night-config:toml", version.ref | ||||
| slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } | ||||
|  | ||||
| # Minecraft mods | ||||
| fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } | ||||
| fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } | ||||
| fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric-loader" } | ||||
| fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" } | ||||
| create-fabric = { module = "com.simibubi.create:create-fabric-1.20.1", version.ref = "create-fabric" } | ||||
| create-forge = { module = "com.simibubi.create:create-1.20.1", version.ref = "create-forge" } | ||||
| emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" } | ||||
| fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } | ||||
| fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric-loader" } | ||||
| fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } | ||||
| fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" } | ||||
| iris = { module = "maven.modrinth:iris", version.ref = "iris" } | ||||
| jei-api = { module = "mezz.jei:jei-1.21-common-api", version.ref = "jei" } | ||||
| jei-fabric = { module = "mezz.jei:jei-1.21-fabric", version.ref = "jei" } | ||||
|   | ||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip | ||||
| networkTimeout=10000 | ||||
| validateDistributionUrl=true | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
|   | ||||
| @@ -4,9 +4,11 @@ | ||||
| 
 | ||||
| package dan200.computercraft.api; | ||||
| 
 | ||||
| import dan200.computercraft.api.component.ComputerComponent; | ||||
| import dan200.computercraft.api.filesystem.Mount; | ||||
| import dan200.computercraft.api.filesystem.WritableMount; | ||||
| import dan200.computercraft.api.lua.GenericSource; | ||||
| import dan200.computercraft.api.lua.IComputerSystem; | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.lua.ILuaAPIFactory; | ||||
| import dan200.computercraft.api.media.IMedia; | ||||
| @@ -165,7 +167,20 @@ public final class ComputerCraftAPI { | ||||
|      * Register a custom {@link ILuaAPI}, which may be added onto all computers without requiring a peripheral. | ||||
|      * <p> | ||||
|      * Before implementing this interface, consider alternative methods of providing methods. It is generally preferred | ||||
|      * to use peripherals to provide functionality to users. | ||||
|      * to use peripherals to provide functionality to users. If an API is <em>required</em>, you may want to consider | ||||
|      * using {@link ILuaAPI#getModuleName()} to expose this library as a module instead of as a global. | ||||
|      * <p> | ||||
|      * This may be used with {@link IComputerSystem#getComponent(ComputerComponent)} to only attach APIs to specific | ||||
|      * computers. For example, one can add an additional API just to turtles with the following code: | ||||
|      * | ||||
|      * <pre>{@code | ||||
|      * ComputerCraftAPI.registerAPIFactory(computer -> { | ||||
|      *   // Read the turtle component. | ||||
|      *   var turtle = computer.getComponent(ComputerComponents.TURTLE); | ||||
|      *   // If present then add our API. | ||||
|      *   return turtle == null ? null : new MyCustomTurtleApi(turtle); | ||||
|      * }); | ||||
|      * }</pre> | ||||
|      * | ||||
|      * @param factory The factory for your API subclass. | ||||
|      * @see ILuaAPIFactory | ||||
|   | ||||
| @@ -0,0 +1,24 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.api.component; | ||||
| 
 | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| import org.jetbrains.annotations.ApiStatus; | ||||
| 
 | ||||
| /** | ||||
|  * A computer which has permission to perform administrative/op commands, such as the command computer. | ||||
|  */ | ||||
| @ApiStatus.NonExtendable | ||||
| public interface AdminComputer { | ||||
|     /** | ||||
|      * The permission level that this computer can operate at. | ||||
|      * | ||||
|      * @return The permission level for this computer. | ||||
|      * @see CommandSourceStack#hasPermission(int) | ||||
|      */ | ||||
|     default int permissionLevel() { | ||||
|         return 2; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.api.component; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.IComputerSystem; | ||||
| import dan200.computercraft.api.lua.ILuaAPIFactory; | ||||
| 
 | ||||
| /** | ||||
|  * A component attached to a computer. | ||||
|  * <p> | ||||
|  * Components provide a mechanism to attach additional data to a computer, that can then be queried with | ||||
|  * {@link IComputerSystem#getComponent(ComputerComponent)}. | ||||
|  * <p> | ||||
|  * This is largely designed for {@linkplain ILuaAPIFactory custom APIs}, allowing APIs to read additional properties | ||||
|  * of the computer, such as its position. | ||||
|  * | ||||
|  * @param <T> The type of this component. | ||||
|  * @see ComputerComponents The built-in components. | ||||
|  */ | ||||
| @SuppressWarnings("UnusedTypeParameter") | ||||
| public final class ComputerComponent<T> { | ||||
|     private final String id; | ||||
| 
 | ||||
|     private ComputerComponent(String id) { | ||||
|         this.id = id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new computer component. | ||||
|      * <p> | ||||
|      * Mods typically will not need to create their own components. | ||||
|      * | ||||
|      * @param namespace The namespace of this component. This should be the mod id. | ||||
|      * @param id        The unique id of this component. | ||||
|      * @param <T>       The component | ||||
|      * @return The newly created component. | ||||
|      */ | ||||
|     public static <T> ComputerComponent<T> create(String namespace, String id) { | ||||
|         return new ComputerComponent<>(namespace + ":" + id); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return "ComputerComponent(" + id + ")"; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.api.component; | ||||
| 
 | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.api.pocket.IPocketAccess; | ||||
| import dan200.computercraft.api.turtle.ITurtleAccess; | ||||
| 
 | ||||
| /** | ||||
|  * The {@link ComputerComponent}s provided by ComputerCraft. | ||||
|  */ | ||||
| public class ComputerComponents { | ||||
|     /** | ||||
|      * The {@link ITurtleAccess} associated with a turtle. | ||||
|      */ | ||||
|     public static final ComputerComponent<ITurtleAccess> TURTLE = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "turtle"); | ||||
| 
 | ||||
|     /** | ||||
|      * The {@link IPocketAccess} associated with a pocket computer. | ||||
|      */ | ||||
|     public static final ComputerComponent<IPocketAccess> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket"); | ||||
| 
 | ||||
|     /** | ||||
|      * This component is only present on "command computers", and other computers with admin capabilities. | ||||
|      */ | ||||
|     public static final ComputerComponent<AdminComputer> ADMIN_COMPUTER = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "admin_computer"); | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| // SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.api.lua; | ||||
| 
 | ||||
| import dan200.computercraft.api.component.ComputerComponent; | ||||
| import dan200.computercraft.api.peripheral.IComputerAccess; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import org.jetbrains.annotations.ApiStatus; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * An interface passed to {@link ILuaAPIFactory} in order to provide additional information | ||||
|  * about a computer. | ||||
|  */ | ||||
| @ApiStatus.NonExtendable | ||||
| public interface IComputerSystem extends IComputerAccess { | ||||
|     /** | ||||
|      * Get the level this computer is currently in. | ||||
|      * <p> | ||||
|      * This method is not guaranteed to remain the same (even for stationary computers). | ||||
|      * | ||||
|      * @return The computer's current level. | ||||
|      */ | ||||
|     ServerLevel getLevel(); | ||||
| 
 | ||||
|     /** | ||||
|      * Get the position this computer is currently at. | ||||
|      * <p> | ||||
|      * This method is not guaranteed to remain the same (even for stationary computers). | ||||
|      * | ||||
|      * @return The computer's current position. | ||||
|      */ | ||||
|     BlockPos getPosition(); | ||||
| 
 | ||||
|     /** | ||||
|      * Get the label for this computer. | ||||
|      * | ||||
|      * @return This computer's label, or {@code null} if it is not set. | ||||
|      */ | ||||
|     @Nullable | ||||
|     String getLabel(); | ||||
| 
 | ||||
|     /** | ||||
|      * Get a component attached to this computer. | ||||
|      * <p> | ||||
|      * No component is guaranteed to be on a computer, and so this method should always be guarded with a null check. | ||||
|      * <p> | ||||
|      * This method will always return the same value for a given component, and so may be cached. | ||||
|      * | ||||
|      * @param component The component to query. | ||||
|      * @param <T>       The type of the component. | ||||
|      * @return The component, if present. | ||||
|      */ | ||||
|     <T> @Nullable T getComponent(ComputerComponent<T> component); | ||||
| } | ||||
| @@ -4,13 +4,15 @@ | ||||
| 
 | ||||
| package dan200.computercraft.api.lua; | ||||
| 
 | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * Construct an {@link ILuaAPI} for a specific computer. | ||||
|  * Construct an {@link ILuaAPI} for a computer. | ||||
|  * | ||||
|  * @see ILuaAPI | ||||
|  * @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) | ||||
|  * @see ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) | ||||
|  */ | ||||
| @FunctionalInterface | ||||
| public interface ILuaAPIFactory { | ||||
| @@ -5,16 +5,35 @@ | ||||
| package dan200.computercraft.api.pocket; | ||||
| 
 | ||||
| import dan200.computercraft.api.upgrades.UpgradeBase; | ||||
| import dan200.computercraft.api.upgrades.UpgradeData; | ||||
| import net.minecraft.core.component.DataComponentPatch; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import net.minecraft.world.entity.Entity; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| import net.minecraft.world.phys.Vec3; | ||||
| import org.jetbrains.annotations.ApiStatus; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * Wrapper class for pocket computers. | ||||
|  */ | ||||
| @ApiStatus.NonExtendable | ||||
| public interface IPocketAccess { | ||||
|     /** | ||||
|      * Get the level in which the pocket computer exists. | ||||
|      * | ||||
|      * @return The pocket computer's level. | ||||
|      */ | ||||
|     ServerLevel getLevel(); | ||||
| 
 | ||||
|     /** | ||||
|      * Get the position of the pocket computer. | ||||
|      * | ||||
|      * @return The pocket computer's position. | ||||
|      */ | ||||
|     Vec3 getPosition(); | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the entity holding this item. | ||||
|      * <p> | ||||
| @@ -61,6 +80,26 @@ public interface IPocketAccess { | ||||
|      */ | ||||
|     void setLight(int colour); | ||||
| 
 | ||||
|     /** | ||||
|      * Get the currently equipped upgrade. | ||||
|      * | ||||
|      * @return The currently equipped upgrade. | ||||
|      * @see #getUpgradeData() | ||||
|      * @see #setUpgrade(UpgradeData) | ||||
|      */ | ||||
|     @Nullable | ||||
|     UpgradeData<IPocketUpgrade> getUpgrade(); | ||||
| 
 | ||||
|     /** | ||||
|      * Set the upgrade for this pocket computer, also updating the item stack. | ||||
|      * <p> | ||||
|      * Note this method is not thread safe - it must be called from the server thread. | ||||
|      * | ||||
|      * @param upgrade The new upgrade to set it to, may be {@code null}. | ||||
|      * @see #getUpgrade() | ||||
|      */ | ||||
|     void setUpgrade(@Nullable UpgradeData<IPocketUpgrade> upgrade); | ||||
| 
 | ||||
|     /** | ||||
|      * Get the upgrade-specific NBT. | ||||
|      * <p> | ||||
| @@ -70,6 +109,7 @@ public interface IPocketAccess { | ||||
|      * @see #setUpgradeData(DataComponentPatch) | ||||
|      * @see UpgradeBase#getUpgradeItem(DataComponentPatch) | ||||
|      * @see UpgradeBase#getUpgradeData(ItemStack) | ||||
|      * @see #getUpgrade() | ||||
|      */ | ||||
|     DataComponentPatch getUpgradeData(); | ||||
| 
 | ||||
|   | ||||
| @@ -45,6 +45,7 @@ dependencies { | ||||
|     compileOnly(libs.mixin) | ||||
|     compileOnly(libs.mixinExtra) | ||||
|     compileOnly(libs.bundles.externalMods.common) | ||||
|     compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false } | ||||
|     clientCompileOnly(variantOf(libs.emi) { classifier("api") }) | ||||
| 
 | ||||
|     annotationProcessorEverywhere(libs.autoService) | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import java.util.Objects; | ||||
| /** | ||||
|  * The global factory for {@link ILuaAPIFactory}s. | ||||
|  * | ||||
|  * @see dan200.computercraft.core.ComputerContext.Builder#apiFactories(Collection) | ||||
|  * @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) | ||||
|  */ | ||||
| public final class ApiFactories { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import com.mojang.brigadier.arguments.ArgumentType; | ||||
| import com.mojang.serialization.Codec; | ||||
| import com.mojang.serialization.MapCodec; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.api.component.ComputerComponents; | ||||
| import dan200.computercraft.api.detail.DetailProvider; | ||||
| import dan200.computercraft.api.detail.VanillaDetailRegistries; | ||||
| import dan200.computercraft.api.media.IMedia; | ||||
| @@ -26,6 +27,7 @@ import dan200.computercraft.shared.common.ClearColourRecipe; | ||||
| import dan200.computercraft.shared.common.ColourableRecipe; | ||||
| import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider; | ||||
| import dan200.computercraft.shared.common.HeldItemMenu; | ||||
| import dan200.computercraft.shared.computer.apis.CommandAPI; | ||||
| import dan200.computercraft.shared.computer.blocks.CommandComputerBlock; | ||||
| import dan200.computercraft.shared.computer.blocks.ComputerBlock; | ||||
| import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity; | ||||
| @@ -64,6 +66,7 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity; | ||||
| import dan200.computercraft.shared.platform.PlatformHelper; | ||||
| import dan200.computercraft.shared.platform.RegistrationHelper; | ||||
| import dan200.computercraft.shared.platform.RegistryEntry; | ||||
| import dan200.computercraft.shared.pocket.apis.PocketAPI; | ||||
| import dan200.computercraft.shared.pocket.items.PocketComputerItem; | ||||
| import dan200.computercraft.shared.pocket.peripherals.PocketModem; | ||||
| import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker; | ||||
| @@ -73,8 +76,10 @@ import dan200.computercraft.shared.recipe.function.CopyComponents; | ||||
| import dan200.computercraft.shared.recipe.function.RecipeFunction; | ||||
| import dan200.computercraft.shared.turtle.FurnaceRefuelHandler; | ||||
| import dan200.computercraft.shared.turtle.TurtleOverlay; | ||||
| import dan200.computercraft.shared.turtle.apis.TurtleAPI; | ||||
| import dan200.computercraft.shared.turtle.blocks.TurtleBlock; | ||||
| import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity; | ||||
| import dan200.computercraft.shared.turtle.core.TurtleAccessInternal; | ||||
| import dan200.computercraft.shared.turtle.inventory.TurtleMenu; | ||||
| import dan200.computercraft.shared.turtle.items.TurtleItem; | ||||
| import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe; | ||||
| @@ -82,6 +87,7 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable; | ||||
| import dan200.computercraft.shared.turtle.upgrades.TurtleModem; | ||||
| import dan200.computercraft.shared.turtle.upgrades.TurtleSpeaker; | ||||
| import dan200.computercraft.shared.turtle.upgrades.TurtleTool; | ||||
| import dan200.computercraft.shared.util.ComponentMap; | ||||
| import dan200.computercraft.shared.util.DataComponentUtil; | ||||
| import dan200.computercraft.shared.util.NonNegativeId; | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| @@ -114,6 +120,7 @@ import net.minecraft.world.level.block.state.BlockBehaviour; | ||||
| import net.minecraft.world.level.material.MapColor; | ||||
| import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; | ||||
| 
 | ||||
| import java.util.Objects; | ||||
| import java.util.function.BiFunction; | ||||
| import java.util.function.Predicate; | ||||
| import java.util.function.UnaryOperator; | ||||
| @@ -579,6 +586,22 @@ public final class ModRegistry { | ||||
|             return null; | ||||
|         }); | ||||
| 
 | ||||
|         ComputerCraftAPI.registerAPIFactory(computer -> { | ||||
|             var turtle = computer.getComponent(ComputerComponents.TURTLE); | ||||
|             var metrics = Objects.requireNonNull(computer.getComponent(ComponentMap.METRICS)); | ||||
|             return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle); | ||||
|         }); | ||||
| 
 | ||||
|         ComputerCraftAPI.registerAPIFactory(computer -> { | ||||
|             var pocket = computer.getComponent(ComputerComponents.POCKET); | ||||
|             return pocket == null ? null : new PocketAPI(pocket); | ||||
|         }); | ||||
| 
 | ||||
|         ComputerCraftAPI.registerAPIFactory(computer -> { | ||||
|             var admin = computer.getComponent(ComputerComponents.ADMIN_COMPUTER); | ||||
|             return admin == null ? null : new CommandAPI(computer, admin); | ||||
|         }); | ||||
| 
 | ||||
|         VanillaDetailRegistries.ITEM_STACK.addProvider(ItemDetails::fill); | ||||
|         VanillaDetailRegistries.BLOCK_IN_WORLD.addProvider(BlockDetails::fill); | ||||
|     } | ||||
|   | ||||
| @@ -6,11 +6,11 @@ package dan200.computercraft.shared.computer.apis; | ||||
| 
 | ||||
| import com.mojang.brigadier.tree.CommandNode; | ||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | ||||
| import dan200.computercraft.api.component.AdminComputer; | ||||
| import dan200.computercraft.api.detail.BlockReference; | ||||
| import dan200.computercraft.api.detail.VanillaDetailRegistries; | ||||
| import dan200.computercraft.api.lua.*; | ||||
| import dan200.computercraft.core.Logging; | ||||
| import dan200.computercraft.shared.computer.core.ServerComputer; | ||||
| import dan200.computercraft.shared.util.NBTUtil; | ||||
| import net.minecraft.commands.CommandSource; | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| @@ -35,11 +35,13 @@ import java.util.*; | ||||
| public class CommandAPI implements ILuaAPI { | ||||
|     private static final Logger LOG = LoggerFactory.getLogger(CommandAPI.class); | ||||
| 
 | ||||
|     private final ServerComputer computer; | ||||
|     private final IComputerSystem computer; | ||||
|     private final AdminComputer admin; | ||||
|     private final OutputReceiver receiver = new OutputReceiver(); | ||||
| 
 | ||||
|     public CommandAPI(ServerComputer computer) { | ||||
|     public CommandAPI(IComputerSystem computer, AdminComputer admin) { | ||||
|         this.computer = computer; | ||||
|         this.admin = admin; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -295,7 +297,7 @@ public class CommandAPI implements ILuaAPI { | ||||
| 
 | ||||
|         return new CommandSourceStack(receiver, | ||||
|             Vec3.atCenterOf(computer.getPosition()), Vec2.ZERO, | ||||
|             computer.getLevel(), 2, | ||||
|             computer.getLevel(), admin.permissionLevel(), | ||||
|             name, Component.literal(name), | ||||
|             computer.getLevel().getServer(), null | ||||
|         ); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ import javax.annotation.Nullable; | ||||
| import java.util.Objects; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider { | ||||
| public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuProvider { | ||||
|     private static final String NBT_ID = "ComputerId"; | ||||
|     private static final String NBT_LABEL = "Label"; | ||||
|     private static final String NBT_ON = "On"; | ||||
| @@ -326,17 +326,14 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements | ||||
|         for (var dir : DirectionUtil.FACINGS) updateRedstoneTo(dir); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public final int getComputerID() { | ||||
|         return computerID; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public final @Nullable String getLabel() { | ||||
|         return label; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public final void setComputerID(int id) { | ||||
|         if (getLevel().isClientSide || computerID == id) return; | ||||
| 
 | ||||
| @@ -344,7 +341,6 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements | ||||
|         BlockEntityHelpers.updateBlock(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public final void setLabel(@Nullable String label) { | ||||
|         if (getLevel().isClientSide || Objects.equals(this.label, label)) return; | ||||
| 
 | ||||
| @@ -354,7 +350,6 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements | ||||
|         BlockEntityHelpers.updateBlock(this); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public ComputerFamily getFamily() { | ||||
|         return family; | ||||
|     } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import dan200.computercraft.shared.computer.core.ComputerState; | ||||
| import dan200.computercraft.shared.computer.core.ServerComputer; | ||||
| import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory; | ||||
| import dan200.computercraft.shared.config.Config; | ||||
| import dan200.computercraft.shared.util.ComponentMap; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.core.Direction; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| @@ -34,7 +35,8 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity { | ||||
|     protected ServerComputer createComputer(int id) { | ||||
|         return new ServerComputer( | ||||
|             (ServerLevel) getLevel(), getBlockPos(), id, label, | ||||
|             getFamily(), Config.computerTermWidth, Config.computerTermHeight | ||||
|             getFamily(), Config.computerTermWidth, Config.computerTermHeight, | ||||
|             ComponentMap.empty() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -1,22 +0,0 @@ | ||||
| // Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. | ||||
| // | ||||
| // SPDX-License-Identifier: LicenseRef-CCPL | ||||
| 
 | ||||
| package dan200.computercraft.shared.computer.blocks; | ||||
| 
 | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| public interface IComputerBlockEntity { | ||||
|     int getComputerID(); | ||||
| 
 | ||||
|     void setComputerID(int id); | ||||
| 
 | ||||
|     @Nullable | ||||
|     String getLabel(); | ||||
| 
 | ||||
|     void setLabel(@Nullable String label); | ||||
| 
 | ||||
|     ComputerFamily getFamily(); | ||||
| } | ||||
| @@ -0,0 +1,101 @@ | ||||
| // SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.computer.core; | ||||
| 
 | ||||
| import dan200.computercraft.api.component.ComputerComponent; | ||||
| import dan200.computercraft.api.lua.IComputerSystem; | ||||
| import dan200.computercraft.api.lua.ILuaAPIFactory; | ||||
| import dan200.computercraft.api.peripheral.IPeripheral; | ||||
| import dan200.computercraft.core.apis.ComputerAccess; | ||||
| import dan200.computercraft.core.apis.IAPIEnvironment; | ||||
| import dan200.computercraft.core.computer.ApiLifecycle; | ||||
| import dan200.computercraft.shared.util.ComponentMap; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * Implementation of {@link IComputerSystem} for usage by externally registered APIs. | ||||
|  * | ||||
|  * @see ILuaAPIFactory | ||||
|  */ | ||||
| final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle { | ||||
|     private final ServerComputer computer; | ||||
|     private final IAPIEnvironment environment; | ||||
|     private final ComponentMap components; | ||||
| 
 | ||||
|     private boolean active; | ||||
| 
 | ||||
|     ComputerSystem(ServerComputer computer, IAPIEnvironment environment, ComponentMap components) { | ||||
|         super(environment); | ||||
|         this.computer = computer; | ||||
|         this.environment = environment; | ||||
|         this.components = components; | ||||
|     } | ||||
| 
 | ||||
|     void activate() { | ||||
|         active = true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void shutdown() { | ||||
|         unmountAll(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getAttachmentName() { | ||||
|         return "computer"; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public ServerLevel getLevel() { | ||||
|         if (!active) { | ||||
|             throw new IllegalStateException(""" | ||||
|                 Cannot access level when constructing the API. Computers are not guaranteed to stay in one place and | ||||
|                 APIs should not rely on the level remaining constant. Instead, call this method when needed. | ||||
|                 """.replace('\n', ' ').strip() | ||||
|             ); | ||||
|         } | ||||
|         return computer.getLevel(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public BlockPos getPosition() { | ||||
|         if (!active) { | ||||
|             throw new IllegalStateException(""" | ||||
|                 Cannot access computer position when constructing the API. Computers are not guaranteed to stay in one | ||||
|                 place and APIs should not rely on the position remaining constant. Instead, call this method when | ||||
|                 needed. | ||||
|                 """.replace('\n', ' ').strip() | ||||
|             ); | ||||
|         } | ||||
|         return computer.getPosition(); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public String getLabel() { | ||||
|         return environment.getLabel(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Map<String, IPeripheral> getAvailablePeripherals() { | ||||
|         // TODO: Should this return peripherals on the current computer? | ||||
|         return Map.of(); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public IPeripheral getAvailablePeripheral(String name) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <T> @Nullable T getComponent(ComputerComponent<T> component) { | ||||
|         return components.get(component); | ||||
|     } | ||||
| } | ||||
| @@ -5,15 +5,16 @@ | ||||
| package dan200.computercraft.shared.computer.core; | ||||
| 
 | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.api.component.AdminComputer; | ||||
| import dan200.computercraft.api.component.ComputerComponents; | ||||
| import dan200.computercraft.api.filesystem.WritableMount; | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.peripheral.IPeripheral; | ||||
| import dan200.computercraft.api.peripheral.WorkMonitor; | ||||
| import dan200.computercraft.core.computer.Computer; | ||||
| import dan200.computercraft.core.computer.ComputerEnvironment; | ||||
| import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.core.metrics.MetricsObserver; | ||||
| import dan200.computercraft.shared.computer.apis.CommandAPI; | ||||
| import dan200.computercraft.impl.ApiFactories; | ||||
| import dan200.computercraft.shared.computer.menu.ComputerMenu; | ||||
| import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; | ||||
| import dan200.computercraft.shared.computer.terminal.TerminalState; | ||||
| @@ -22,6 +23,7 @@ import dan200.computercraft.shared.network.NetworkMessage; | ||||
| import dan200.computercraft.shared.network.client.ClientNetworkContext; | ||||
| import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage; | ||||
| import dan200.computercraft.shared.network.server.ServerNetworking; | ||||
| import dan200.computercraft.shared.util.ComponentMap; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import net.minecraft.world.entity.player.Player; | ||||
| @@ -48,7 +50,8 @@ public class ServerComputer implements InputHandler, ComputerEnvironment { | ||||
|     private int ticksSincePing; | ||||
| 
 | ||||
|     public ServerComputer( | ||||
|         ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight | ||||
|         ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight, | ||||
|         ComponentMap baseComponents | ||||
|     ) { | ||||
|         this.level = level; | ||||
|         this.position = position; | ||||
| @@ -58,10 +61,27 @@ public class ServerComputer implements InputHandler, ComputerEnvironment { | ||||
|         terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged); | ||||
|         metrics = context.metrics().createMetricObserver(this); | ||||
| 
 | ||||
|         var componentBuilder = ComponentMap.builder(); | ||||
|         componentBuilder.add(ComponentMap.METRICS, metrics); | ||||
|         if (family == ComputerFamily.COMMAND) { | ||||
|             componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() { | ||||
|             }); | ||||
|         } | ||||
|         componentBuilder.add(baseComponents); | ||||
|         var components = componentBuilder.build(); | ||||
| 
 | ||||
|         computer = new Computer(context.computerContext(), this, terminal, computerID); | ||||
|         computer.setLabel(label); | ||||
| 
 | ||||
|         if (family == ComputerFamily.COMMAND) addAPI(new CommandAPI(this)); | ||||
|         // Load in the externally registered APIs. | ||||
|         for (var factory : ApiFactories.getAll()) { | ||||
|             var system = new ComputerSystem(this, computer.getAPIEnvironment(), components); | ||||
|             var api = factory.create(system); | ||||
|             if (api == null) continue; | ||||
| 
 | ||||
|             system.activate(); | ||||
|             computer.addApi(api, system); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public ComputerFamily getFamily() { | ||||
| @@ -211,10 +231,6 @@ public class ServerComputer implements InputHandler, ComputerEnvironment { | ||||
|         computer.getEnvironment().setBundledRedstoneInput(side, combination); | ||||
|     } | ||||
| 
 | ||||
|     public void addAPI(ILuaAPI api) { | ||||
|         computer.addApi(api); | ||||
|     } | ||||
| 
 | ||||
|     public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) { | ||||
|         computer.getEnvironment().setPeripheral(side, peripheral); | ||||
|     } | ||||
|   | ||||
| @@ -17,7 +17,6 @@ import dan200.computercraft.core.lua.ILuaMachine; | ||||
| import dan200.computercraft.core.methods.MethodSupplier; | ||||
| import dan200.computercraft.core.methods.PeripheralMethod; | ||||
| import dan200.computercraft.impl.AbstractComputerCraftAPI; | ||||
| import dan200.computercraft.impl.ApiFactories; | ||||
| import dan200.computercraft.impl.GenericSources; | ||||
| import dan200.computercraft.shared.CommonHooks; | ||||
| import dan200.computercraft.shared.computer.metrics.GlobalMetrics; | ||||
| @@ -74,7 +73,6 @@ public final class ServerContext { | ||||
|             .computerThreads(ConfigSpec.computerThreads.get()) | ||||
|             .mainThreadScheduler(mainThread) | ||||
|             .luaFactory(luaMachine) | ||||
|             .apiFactories(ApiFactories.getAll()) | ||||
|             .genericMethods(GenericSources.getAllMethods()) | ||||
|             .build(); | ||||
|         idAssigner = new IDAssigner(storageDir.resolve("ids.json")); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| package dan200.computercraft.shared.data; | ||||
| 
 | ||||
| import dan200.computercraft.shared.ModRegistry; | ||||
| import dan200.computercraft.shared.computer.blocks.IComputerBlockEntity; | ||||
| import dan200.computercraft.shared.computer.blocks.AbstractComputerBlockEntity; | ||||
| import net.minecraft.world.level.storage.loot.LootContext; | ||||
| import net.minecraft.world.level.storage.loot.parameters.LootContextParam; | ||||
| import net.minecraft.world.level.storage.loot.parameters.LootContextParams; | ||||
| @@ -27,7 +27,7 @@ public final class HasComputerIdLootCondition implements LootItemCondition { | ||||
|     @Override | ||||
|     public boolean test(LootContext lootContext) { | ||||
|         var tile = lootContext.getParamOrNull(LootContextParams.BLOCK_ENTITY); | ||||
|         return tile instanceof IComputerBlockEntity computer && computer.getComputerID() >= 0; | ||||
|         return tile instanceof AbstractComputerBlockEntity computer && computer.getComputerID() >= 0; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|   | ||||
| @@ -0,0 +1,34 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.integration; | ||||
| 
 | ||||
| import com.simibubi.create.content.contraptions.BlockMovementChecks; | ||||
| import com.simibubi.create.content.contraptions.BlockMovementChecks.CheckResult; | ||||
| import dan200.computercraft.shared.peripheral.modem.wired.CableBlock; | ||||
| import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock; | ||||
| 
 | ||||
| /** | ||||
|  * Integration with Create. | ||||
|  */ | ||||
| public final class CreateIntegration { | ||||
|     public static final String ID = "create"; | ||||
| 
 | ||||
|     private CreateIntegration() { | ||||
|     } | ||||
| 
 | ||||
|     public static void setup() { | ||||
|         // Allow modems to be treated as "attached" to their adjacent block. | ||||
|         BlockMovementChecks.registerAttachedCheck((state, world, pos, direction) -> { | ||||
|             var block = state.getBlock(); | ||||
|             if (block instanceof WirelessModemBlock) { | ||||
|                 return CheckResult.of(state.getValue(WirelessModemBlock.FACING) == direction); | ||||
|             } else if (block instanceof CableBlock) { | ||||
|                 return CheckResult.of(state.getValue(CableBlock.MODEM).getFacing() == direction); | ||||
|             } else { | ||||
|                 return CheckResult.PASS; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -8,6 +8,7 @@ import net.minecraft.core.registries.Registries; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import net.minecraft.tags.TagKey; | ||||
| import net.minecraft.world.level.block.Block; | ||||
| import net.minecraft.world.level.block.state.BlockState; | ||||
| 
 | ||||
| /** | ||||
|  * Tags defined by external mods. | ||||
| @@ -26,9 +27,9 @@ public final class ExternalModTags { | ||||
|         /** | ||||
|          * Create's "brittle" tag, used to determine if this block needs to be moved before its neighbours. | ||||
|          * | ||||
|          * @see <a href="https://github.com/Creators-of-Create/Create/blob/mc1.20.1/dev/src/main/java/com/simibubi/create/content/contraptions/BlockMovementChecks.java">{@code BlockMovementChecks}</a> | ||||
|          * @see com.simibubi.create.content.contraptions.BlockMovementChecks#isBrittle(BlockState) | ||||
|          */ | ||||
|         public static final TagKey<Block> CREATE_BRITTLE = make("create", "brittle"); | ||||
|         public static final TagKey<Block> CREATE_BRITTLE = make(CreateIntegration.ID, "brittle"); | ||||
| 
 | ||||
|         private static TagKey<Block> make(String mod, String name) { | ||||
|             return TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(mod, name)); | ||||
|   | ||||
| @@ -43,7 +43,7 @@ public record PocketComputerDataMessage( | ||||
|         this( | ||||
|             computer.getInstanceUUID(), | ||||
|             computer.getState(), | ||||
|             computer.getLight(), | ||||
|             computer.getBrain().getLight(), | ||||
|             sendTerminal ? Optional.of(computer.getTerminalState()) : Optional.empty() | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -276,16 +276,18 @@ public abstract class SpeakerPeripheral implements IPeripheral { | ||||
|      * Attempt to stream some audio data to the speaker. | ||||
|      * <p> | ||||
|      * This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer | ||||
|      * and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for | ||||
|      * a [`speaker_audio_empty`] event before trying again. | ||||
|      * and played back at 48kHz. If this buffer is full, this function will return {@literal false}. Programs should | ||||
|      * wait for a [`speaker_audio_empty`] event before trying to play audio again. | ||||
|      * <p> | ||||
|      * > [!NOTE] | ||||
|      * > The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small | ||||
|      * > number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible | ||||
|      * > (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or | ||||
|      * > computer is lagging. | ||||
|      * The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small | ||||
|      * number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible | ||||
|      * (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or | ||||
|      * computer is lagging. | ||||
|      * <p> | ||||
|      * [`speaker_audio`] provides a more complete guide to using speakers | ||||
|      * While the speaker accepts 8-bit PCM audio, the audio stream is re-encoded before being played. This means that | ||||
|      * the supplied samples may not be played out exactly. | ||||
|      * <p> | ||||
|      * [`speaker_audio`] provides a more complete guide to using speakers. | ||||
|      * | ||||
|      * @param context The Lua context. | ||||
|      * @param audio   The audio data to play. | ||||
|   | ||||
| @@ -6,10 +6,10 @@ package dan200.computercraft.shared.pocket.apis; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.lua.LuaFunction; | ||||
| import dan200.computercraft.api.pocket.IPocketAccess; | ||||
| import dan200.computercraft.api.pocket.IPocketUpgrade; | ||||
| import dan200.computercraft.api.upgrades.UpgradeData; | ||||
| import dan200.computercraft.impl.PocketUpgrades; | ||||
| import dan200.computercraft.shared.pocket.core.PocketServerComputer; | ||||
| import net.minecraft.core.NonNullList; | ||||
| import net.minecraft.world.entity.player.Player; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| @@ -34,10 +34,10 @@ import java.util.Objects; | ||||
|  * @cc.module pocket | ||||
|  */ | ||||
| public class PocketAPI implements ILuaAPI { | ||||
|     private final PocketServerComputer computer; | ||||
|     private final IPocketAccess pocket; | ||||
| 
 | ||||
|     public PocketAPI(PocketServerComputer computer) { | ||||
|         this.computer = computer; | ||||
|     public PocketAPI(IPocketAccess pocket) { | ||||
|         this.pocket = pocket; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -56,10 +56,10 @@ public class PocketAPI implements ILuaAPI { | ||||
|      */ | ||||
|     @LuaFunction(mainThread = true) | ||||
|     public final Object[] equipBack() { | ||||
|         var entity = computer.getEntity(); | ||||
|         var entity = pocket.getEntity(); | ||||
|         if (!(entity instanceof Player player)) return new Object[]{ false, "Cannot find player" }; | ||||
|         var inventory = player.getInventory(); | ||||
|         var previousUpgrade = computer.getUpgrade(); | ||||
|         var previousUpgrade = pocket.getUpgrade(); | ||||
| 
 | ||||
|         // Attempt to find the upgrade, starting in the main segment, and then looking in the opposite | ||||
|         // one. We start from the position the item is currently in and loop round to the start. | ||||
| @@ -73,7 +73,7 @@ public class PocketAPI implements ILuaAPI { | ||||
|         if (previousUpgrade != null) storeItem(player, previousUpgrade.getUpgradeItem()); | ||||
| 
 | ||||
|         // Set the new upgrade | ||||
|         computer.setUpgrade(newUpgrade); | ||||
|         pocket.setUpgrade(newUpgrade); | ||||
| 
 | ||||
|         return new Object[]{ true }; | ||||
|     } | ||||
| @@ -87,13 +87,13 @@ public class PocketAPI implements ILuaAPI { | ||||
|      */ | ||||
|     @LuaFunction(mainThread = true) | ||||
|     public final Object[] unequipBack() { | ||||
|         var entity = computer.getEntity(); | ||||
|         var entity = pocket.getEntity(); | ||||
|         if (!(entity instanceof Player player)) return new Object[]{ false, "Cannot find player" }; | ||||
|         var previousUpgrade = computer.getUpgrade(); | ||||
|         var previousUpgrade = pocket.getUpgrade(); | ||||
| 
 | ||||
|         if (previousUpgrade == null) return new Object[]{ false, "Nothing to unequip" }; | ||||
| 
 | ||||
|         computer.setUpgrade(null); | ||||
|         pocket.setUpgrade(null); | ||||
| 
 | ||||
|         storeItem(player, previousUpgrade.getUpgradeItem()); | ||||
| 
 | ||||
| @@ -111,7 +111,7 @@ public class PocketAPI implements ILuaAPI { | ||||
|         for (var i = 0; i < inv.size(); i++) { | ||||
|             var invStack = inv.get((i + start) % inv.size()); | ||||
|             if (!invStack.isEmpty()) { | ||||
|                 var newUpgrade = PocketUpgrades.instance().get(computer.getLevel().registryAccess(), invStack); | ||||
|                 var newUpgrade = PocketUpgrades.instance().get(pocket.getLevel().registryAccess(), invStack); | ||||
| 
 | ||||
|                 if (newUpgrade != null && !Objects.equals(newUpgrade, previous)) { | ||||
|                     // Consume an item from this stack and exit the loop | ||||
|   | ||||
| @@ -0,0 +1,176 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.pocket.core; | ||||
| 
 | ||||
| import dan200.computercraft.api.pocket.IPocketAccess; | ||||
| import dan200.computercraft.api.pocket.IPocketUpgrade; | ||||
| import dan200.computercraft.api.upgrades.UpgradeData; | ||||
| import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import dan200.computercraft.shared.network.client.PocketComputerDataMessage; | ||||
| import dan200.computercraft.shared.network.server.ServerNetworking; | ||||
| import dan200.computercraft.shared.pocket.items.PocketComputerItem; | ||||
| import net.minecraft.core.component.DataComponentPatch; | ||||
| import net.minecraft.core.component.DataComponents; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import net.minecraft.world.entity.Entity; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| import net.minecraft.world.item.component.DyedItemColor; | ||||
| import net.minecraft.world.phys.Vec3; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * Holds additional state for a pocket computer. This includes pocket computer upgrade, | ||||
|  * {@linkplain IPocketAccess#getLight() light colour} and {@linkplain IPocketAccess#getColour() colour}. | ||||
|  * <p> | ||||
|  * This state is read when the brain is created, and written back to the holding item stack when the holding entity is | ||||
|  * ticked (see {@link #updateItem(ItemStack)}). | ||||
|  */ | ||||
| public final class PocketBrain implements IPocketAccess { | ||||
|     private final PocketServerComputer computer; | ||||
| 
 | ||||
|     private PocketHolder holder; | ||||
|     private Vec3 position; | ||||
| 
 | ||||
|     private boolean dirty = false; | ||||
|     private @Nullable UpgradeData<IPocketUpgrade> upgrade; | ||||
|     private int colour = -1; | ||||
|     private int lightColour = -1; | ||||
| 
 | ||||
|     public PocketBrain(PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) { | ||||
|         this.computer = new PocketServerComputer(this, holder, computerID, label, family); | ||||
|         this.holder = holder; | ||||
|         this.position = holder.pos(); | ||||
|         this.upgrade = upgrade; | ||||
|         invalidatePeripheral(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the corresponding pocket computer for this brain. | ||||
|      * | ||||
|      * @return The pocket computer. | ||||
|      */ | ||||
|     public PocketServerComputer computer() { | ||||
|         return computer; | ||||
|     } | ||||
| 
 | ||||
|     PocketHolder holder() { | ||||
|         return holder; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update the position and holder for this computer. | ||||
|      * | ||||
|      * @param newHolder The new holder | ||||
|      */ | ||||
|     public void updateHolder(PocketHolder newHolder) { | ||||
|         position = newHolder.pos(); | ||||
|         computer.setPosition(newHolder.level(), newHolder.blockPos()); | ||||
| 
 | ||||
|         var oldHolder = this.holder; | ||||
|         if (holder.equals(newHolder)) return; | ||||
|         holder = newHolder; | ||||
| 
 | ||||
|         // If a new player has picked it up then rebroadcast the terminal to them | ||||
|         var oldPlayer = oldHolder instanceof PocketHolder.PlayerHolder p ? p.entity() : null; | ||||
|         if (newHolder instanceof PocketHolder.PlayerHolder player && player.entity() != oldPlayer) { | ||||
|             ServerNetworking.sendToPlayer(new PocketComputerDataMessage(computer, true), player.entity()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Write back properties of the pocket brain to the item. | ||||
|      * | ||||
|      * @param stack The pocket computer stack to update. | ||||
|      * @return Whether the item was changed. | ||||
|      */ | ||||
|     public boolean updateItem(ItemStack stack) { | ||||
|         if (!dirty) return false; | ||||
|         this.dirty = false; | ||||
| 
 | ||||
|         stack.set(DataComponents.DYED_COLOR, colour == -1 ? null : new DyedItemColor(colour, false)); | ||||
|         PocketComputerItem.setUpgrade(stack, upgrade); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public ServerLevel getLevel() { | ||||
|         return computer.getLevel(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Vec3 getPosition() { | ||||
|         // This method can be called from off-thread, and so we must use the cached position rather than rereading | ||||
|         // from the holder. | ||||
|         return position; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public @Nullable Entity getEntity() { | ||||
|         return holder instanceof PocketHolder.EntityHolder entity && holder.isValid(computer) ? entity.entity() : null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getColour() { | ||||
|         return colour; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setColour(int colour) { | ||||
|         if (this.colour == colour) return; | ||||
|         dirty = true; | ||||
|         this.colour = colour; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLight() { | ||||
|         return lightColour; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setLight(int colour) { | ||||
|         if (colour < 0 || colour > 0xFFFFFF) colour = -1; | ||||
|         lightColour = colour; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public DataComponentPatch getUpgradeData() { | ||||
|         var upgrade = this.upgrade; | ||||
|         return upgrade == null ? DataComponentPatch.EMPTY : upgrade.data(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setUpgradeData(DataComponentPatch data) { | ||||
|         var upgrade = this.upgrade; | ||||
|         if (upgrade == null) return; | ||||
|         this.upgrade = UpgradeData.of(upgrade.holder(), data); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void invalidatePeripheral() { | ||||
|         var peripheral = upgrade == null ? null : upgrade.upgrade().createPeripheral(this); | ||||
|         computer.setPeripheral(ComputerSide.BACK, peripheral); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() { | ||||
|         return upgrade; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the upgrade for this pocket computer, also updating the item stack. | ||||
|      * <p> | ||||
|      * Note this method is not thread safe - it must be called from the server thread. | ||||
|      * | ||||
|      * @param upgrade The new upgrade to set it to, may be {@code null}. | ||||
|      */ | ||||
|     @Override | ||||
|     public void setUpgrade(@Nullable UpgradeData<IPocketUpgrade> upgrade) { | ||||
|         this.upgrade = upgrade; | ||||
|         dirty = true; | ||||
|         invalidatePeripheral(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,115 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.pocket.core; | ||||
| 
 | ||||
| import dan200.computercraft.shared.computer.core.ServerComputer; | ||||
| import dan200.computercraft.shared.pocket.items.PocketComputerItem; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import net.minecraft.server.level.ServerPlayer; | ||||
| import net.minecraft.world.entity.Entity; | ||||
| import net.minecraft.world.entity.item.ItemEntity; | ||||
| import net.minecraft.world.phys.Vec3; | ||||
| 
 | ||||
| /** | ||||
|  * An object that holds a pocket computer item. | ||||
|  */ | ||||
| public sealed interface PocketHolder permits PocketHolder.EntityHolder { | ||||
|     /** | ||||
|      * The level this holder is in. | ||||
|      * | ||||
|      * @return The holder's level. | ||||
|      */ | ||||
|     ServerLevel level(); | ||||
| 
 | ||||
|     /** | ||||
|      * The position of this holder. | ||||
|      * | ||||
|      * @return The position of this holder. | ||||
|      */ | ||||
|     Vec3 pos(); | ||||
| 
 | ||||
|     /** | ||||
|      * The block position of this holder. | ||||
|      * | ||||
|      * @return The position of this holder. | ||||
|      */ | ||||
|     BlockPos blockPos(); | ||||
| 
 | ||||
|     /** | ||||
|      * Determine if this holder is still valid for a particular computer. | ||||
|      * | ||||
|      * @param computer The current computer. | ||||
|      * @return Whether this holder is valid. | ||||
|      */ | ||||
|     boolean isValid(ServerComputer computer); | ||||
| 
 | ||||
|     /** | ||||
|      * Mark the pocket computer item as having changed. | ||||
|      */ | ||||
|     void setChanged(); | ||||
| 
 | ||||
|     /** | ||||
|      * An {@link Entity} holding a pocket computer. | ||||
|      */ | ||||
|     sealed interface EntityHolder extends PocketHolder permits PocketHolder.PlayerHolder, PocketHolder.ItemEntityHolder { | ||||
|         /** | ||||
|          * Get the entity holding this pocket computer. | ||||
|          * | ||||
|          * @return The holding entity. | ||||
|          */ | ||||
|         Entity entity(); | ||||
| 
 | ||||
|         @Override | ||||
|         default ServerLevel level() { | ||||
|             return (ServerLevel) entity().level(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         default Vec3 pos() { | ||||
|             return entity().getEyePosition(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         default BlockPos blockPos() { | ||||
|             return entity().blockPosition(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A pocket computer in a player's slot. | ||||
|      * | ||||
|      * @param entity The current player. | ||||
|      * @param slot   The slot the pocket computer is in. | ||||
|      */ | ||||
|     record PlayerHolder(ServerPlayer entity, int slot) implements EntityHolder { | ||||
|         @Override | ||||
|         public boolean isValid(ServerComputer computer) { | ||||
|             return entity().isAlive() && PocketComputerItem.isServerComputer(computer, entity().getInventory().getItem(this.slot())); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void setChanged() { | ||||
|             entity.getInventory().setChanged(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A pocket computer in an {@link ItemEntity}. | ||||
|      * | ||||
|      * @param entity The item entity. | ||||
|      */ | ||||
|     record ItemEntityHolder(ItemEntity entity) implements EntityHolder { | ||||
|         @Override | ||||
|         public boolean isValid(ServerComputer computer) { | ||||
|             return entity().isAlive() && PocketComputerItem.isServerComputer(computer, this.entity().getItem()); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void setChanged() { | ||||
|             entity.setItem(entity.getItem().copy()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -4,11 +4,7 @@ | ||||
| 
 | ||||
| package dan200.computercraft.shared.pocket.core; | ||||
| 
 | ||||
| import dan200.computercraft.api.pocket.IPocketAccess; | ||||
| import dan200.computercraft.api.pocket.IPocketUpgrade; | ||||
| import dan200.computercraft.api.upgrades.UpgradeData; | ||||
| import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.shared.ModRegistry; | ||||
| import dan200.computercraft.api.component.ComputerComponents; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import dan200.computercraft.shared.computer.core.ComputerState; | ||||
| import dan200.computercraft.shared.computer.core.ServerComputer; | ||||
| @@ -17,176 +13,81 @@ import dan200.computercraft.shared.network.client.PocketComputerDataMessage; | ||||
| import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage; | ||||
| import dan200.computercraft.shared.network.server.ServerNetworking; | ||||
| import dan200.computercraft.shared.pocket.items.PocketComputerItem; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.core.component.DataComponentPatch; | ||||
| import net.minecraft.core.component.DataComponents; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import dan200.computercraft.shared.util.ComponentMap; | ||||
| import net.minecraft.server.level.ServerPlayer; | ||||
| import net.minecraft.world.entity.Entity; | ||||
| import net.minecraft.world.entity.LivingEntity; | ||||
| import net.minecraft.world.entity.item.ItemEntity; | ||||
| import net.minecraft.world.entity.player.Player; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| import net.minecraft.world.item.component.DyedItemColor; | ||||
| import net.minecraft.world.level.ChunkPos; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| public class PocketServerComputer extends ServerComputer implements IPocketAccess { | ||||
|     private @Nullable IPocketUpgrade upgrade; | ||||
|     private @Nullable Entity entity; | ||||
|     private ItemStack stack = ItemStack.EMPTY; | ||||
| 
 | ||||
|     private int lightColour = -1; | ||||
| /** | ||||
|  * A {@link ServerComputer}-subclass for {@linkplain PocketComputerItem pocket computers}. | ||||
|  * <p> | ||||
|  * This extends default {@link ServerComputer} behaviour by also syncing pocket computer state to nearby players, and | ||||
|  * syncing the terminal to the current player. | ||||
|  * <p> | ||||
|  * The actual pocket computer state (upgrade, light) is maintained in {@link PocketBrain}. The two classes are tightly | ||||
|  * coupled, and maintain a reference to each other. | ||||
|  * | ||||
|  * @see PocketComputerDataMessage | ||||
|  * @see PocketComputerDeletedClientMessage | ||||
|  */ | ||||
| public final class PocketServerComputer extends ServerComputer { | ||||
|     private final PocketBrain brain; | ||||
| 
 | ||||
|     // The state the previous tick, used to determine if the state needs to be sent to the client. | ||||
|     private int oldLightColour = -1; | ||||
|     private @Nullable ComputerState oldComputerState; | ||||
| 
 | ||||
|     private final Set<ServerPlayer> tracking = new HashSet<>(); | ||||
|     private Set<ServerPlayer> tracking = Set.of(); | ||||
| 
 | ||||
|     public PocketServerComputer(ServerLevel world, BlockPos position, int computerID, @Nullable String label, ComputerFamily family) { | ||||
|         super(world, position, computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight); | ||||
|     PocketServerComputer(PocketBrain brain, PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family) { | ||||
|         super( | ||||
|             holder.level(), holder.blockPos(), computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight, | ||||
|             ComponentMap.builder().add(ComputerComponents.POCKET, brain).build() | ||||
|         ); | ||||
|         this.brain = brain; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public Entity getEntity() { | ||||
|         var entity = this.entity; | ||||
|         if (entity == null || stack.isEmpty() || !entity.isAlive()) return null; | ||||
| 
 | ||||
|         if (entity instanceof Player) { | ||||
|             var inventory = ((Player) entity).getInventory(); | ||||
|             return inventory.items.contains(stack) || inventory.offhand.contains(stack) ? entity : null; | ||||
|         } else if (entity instanceof LivingEntity living) { | ||||
|             return living.getMainHandItem() == stack || living.getOffhandItem() == stack ? entity : null; | ||||
|         } else if (entity instanceof ItemEntity itemEntity) { | ||||
|             return itemEntity.getItem() == stack ? entity : null; | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getColour() { | ||||
|         return DyedItemColor.getOrDefault(stack, -1); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setColour(int colour) { | ||||
|         stack.set(DataComponents.DYED_COLOR, colour == -1 ? null : new DyedItemColor(colour, false)); | ||||
|         setItemChanged(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLight() { | ||||
|         return lightColour; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setLight(int colour) { | ||||
|         if (colour < 0 || colour > 0xFFFFFF) colour = -1; | ||||
|         lightColour = colour; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public DataComponentPatch getUpgradeData() { | ||||
|         var upgrade = PocketComputerItem.getUpgradeWithData(stack); | ||||
|         return upgrade == null ? DataComponentPatch.EMPTY : upgrade.data(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setUpgradeData(DataComponentPatch data) { | ||||
|         var upgrade = PocketComputerItem.getUpgradeWithData(stack); | ||||
|         if (upgrade == null) return; | ||||
| 
 | ||||
|         PocketComputerItem.setUpgrade(stack, new UpgradeData<>(upgrade.holder(), data)); | ||||
|         setItemChanged(); | ||||
|     } | ||||
| 
 | ||||
|     private void setItemChanged() { | ||||
|         if (entity instanceof Player player) player.getInventory().setChanged(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void invalidatePeripheral() { | ||||
|         var peripheral = upgrade == null ? null : upgrade.createPeripheral(this); | ||||
|         setPeripheral(ComputerSide.BACK, peripheral); | ||||
|     } | ||||
| 
 | ||||
|     public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() { | ||||
|         return PocketComputerItem.getUpgradeWithData(stack); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the upgrade for this pocket computer, also updating the item stack. | ||||
|      * <p> | ||||
|      * Note this method is not thread safe - it must be called from the server thread. | ||||
|      * | ||||
|      * @param upgrade The new upgrade to set it to, may be {@code null}. | ||||
|      */ | ||||
|     public void setUpgrade(@Nullable UpgradeData<IPocketUpgrade> upgrade) { | ||||
|         synchronized (this) { | ||||
|             stack.set(ModRegistry.DataComponents.POCKET_UPGRADE.get(), upgrade); | ||||
|             setItemChanged(); | ||||
|             this.upgrade = upgrade == null ? null : upgrade.upgrade(); | ||||
|             invalidatePeripheral(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public synchronized void updateValues(@Nullable Entity entity, ItemStack stack, @Nullable IPocketUpgrade upgrade) { | ||||
|         if (entity != null) setPosition((ServerLevel) entity.level(), entity.blockPosition()); | ||||
| 
 | ||||
|         // If a new entity has picked it up then rebroadcast the terminal to them | ||||
|         if (entity != this.entity && entity instanceof ServerPlayer) markTerminalChanged(); | ||||
| 
 | ||||
|         this.entity = entity; | ||||
|         this.stack = stack; | ||||
| 
 | ||||
|         if (this.upgrade != upgrade) { | ||||
|             this.upgrade = upgrade; | ||||
|             invalidatePeripheral(); | ||||
|         } | ||||
|     public PocketBrain getBrain() { | ||||
|         return brain; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void tickServer() { | ||||
|         super.tickServer(); | ||||
| 
 | ||||
|         // Find any players which have gone missing and remove them from the tracking list. | ||||
|         tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel()); | ||||
|         // Get the new set of players tracking the current position. | ||||
|         var newTracking = getLevel().getChunkSource().chunkMap.getPlayers(new ChunkPos(getPosition()), false); | ||||
|         var trackingChanged = tracking.size() != newTracking.size() || !tracking.containsAll(newTracking); | ||||
| 
 | ||||
|         // And now find any new players, add them to the tracking list, and broadcast state where appropriate. | ||||
|         var state = getState(); | ||||
|         if (oldLightColour != lightColour || oldComputerState != state) { | ||||
|         var light = brain.getLight(); | ||||
|         if (oldLightColour != light || oldComputerState != state) { | ||||
|             oldComputerState = state; | ||||
|             oldLightColour = lightColour; | ||||
|             oldLightColour = light; | ||||
| 
 | ||||
|             // Broadcast the state to all players | ||||
|             tracking.addAll(getLevel().players()); | ||||
|             ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking); | ||||
|         } else { | ||||
|             ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), newTracking); | ||||
|         } else if (trackingChanged) { | ||||
|             // Broadcast the state to new players. | ||||
|             List<ServerPlayer> added = new ArrayList<>(); | ||||
|             for (var player : getLevel().players()) { | ||||
|                 if (tracking.add(player)) added.add(player); | ||||
|             } | ||||
|             var added = newTracking.stream().filter(x -> !tracking.contains(x)).toList(); | ||||
|             if (!added.isEmpty()) { | ||||
|                 ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (trackingChanged) tracking = Set.copyOf(newTracking); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onTerminalChanged() { | ||||
|         super.onTerminalChanged(); | ||||
| 
 | ||||
|         if (entity instanceof ServerPlayer player && entity.isAlive()) { | ||||
|         if (brain.holder() instanceof PocketHolder.PlayerHolder holder && holder.isValid(this)) { | ||||
|             // Broadcast the terminal to the current player. | ||||
|             ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), player); | ||||
|             ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), holder.entity()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -14,22 +14,26 @@ import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.impl.PocketUpgrades; | ||||
| import dan200.computercraft.shared.ModRegistry; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import dan200.computercraft.shared.computer.core.ServerComputer; | ||||
| import dan200.computercraft.shared.computer.core.ServerComputerRegistry; | ||||
| import dan200.computercraft.shared.computer.core.ServerContext; | ||||
| import dan200.computercraft.shared.computer.items.ServerComputerReference; | ||||
| import dan200.computercraft.shared.config.Config; | ||||
| import dan200.computercraft.shared.network.container.ComputerContainerData; | ||||
| import dan200.computercraft.shared.pocket.apis.PocketAPI; | ||||
| import dan200.computercraft.shared.pocket.core.PocketBrain; | ||||
| import dan200.computercraft.shared.pocket.core.PocketHolder; | ||||
| import dan200.computercraft.shared.pocket.core.PocketServerComputer; | ||||
| import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider; | ||||
| import dan200.computercraft.shared.util.DataComponentUtil; | ||||
| import dan200.computercraft.shared.util.IDAssigner; | ||||
| import dan200.computercraft.shared.util.InventoryUtil; | ||||
| import dan200.computercraft.shared.util.NonNegativeId; | ||||
| import net.minecraft.ChatFormatting; | ||||
| import net.minecraft.core.HolderLookup; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.server.MinecraftServer; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import net.minecraft.world.Container; | ||||
| import net.minecraft.server.level.ServerPlayer; | ||||
| import net.minecraft.world.InteractionHand; | ||||
| import net.minecraft.world.InteractionResult; | ||||
| import net.minecraft.world.InteractionResultHolder; | ||||
| @@ -53,12 +57,33 @@ public class PocketComputerItem extends Item implements IMedia { | ||||
|         this.family = family; | ||||
|     } | ||||
| 
 | ||||
|     private boolean tick(ItemStack stack, Entity entity, PocketServerComputer computer) { | ||||
|         var upgrade = getUpgrade(stack); | ||||
|     /** | ||||
|      * Tick a pocket computer. | ||||
|      * | ||||
|      * @param stack  The current pocket computer stack. | ||||
|      * @param holder The entity holding the pocket item. | ||||
|      * @param brain  The pocket computer brain. | ||||
|      */ | ||||
|     private void tick(ItemStack stack, PocketHolder holder, PocketBrain brain) { | ||||
|         brain.updateHolder(holder); | ||||
| 
 | ||||
|         computer.updateValues(entity, stack, upgrade); | ||||
|         // Update pocket upgrade | ||||
|         var upgrade = brain.getUpgrade(); | ||||
|         if (upgrade != null) upgrade.upgrade().update(brain, brain.computer().getPeripheral(ComputerSide.BACK)); | ||||
| 
 | ||||
|         var changed = false; | ||||
|         if (updateItem(stack, brain)) holder.setChanged(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Copy properties from the brain back to the item stack. | ||||
|      * | ||||
|      * @param stack The current pocket computer stack. | ||||
|      * @param brain The current pocket brain. | ||||
|      * @return Whether the item was changed. | ||||
|      */ | ||||
|     private boolean updateItem(ItemStack stack, PocketBrain brain) { | ||||
|         var changed = brain.updateItem(stack); | ||||
|         var computer = brain.computer(); | ||||
| 
 | ||||
|         // Sync label | ||||
|         var label = computer.getLabel(); | ||||
| @@ -73,21 +98,20 @@ public class PocketComputerItem extends Item implements IMedia { | ||||
|             stack.set(ModRegistry.DataComponents.ON.get(), on); | ||||
|         } | ||||
| 
 | ||||
|         // Update pocket upgrade | ||||
|         if (upgrade != null) upgrade.update(computer, computer.getPeripheral(ComputerSide.BACK)); | ||||
| 
 | ||||
|         return changed; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void inventoryTick(ItemStack stack, Level world, Entity entity, int slotNum, boolean selected) { | ||||
|         if (world.isClientSide) return; | ||||
|         Container inventory = entity instanceof Player player ? player.getInventory() : null; | ||||
|         var computer = createServerComputer((ServerLevel) world, entity, inventory, stack); | ||||
|         computer.keepAlive(); | ||||
|         // This (in vanilla at least) is only called for players. Don't bother to handle other entities. | ||||
|         if (world.isClientSide || !(entity instanceof ServerPlayer player)) return; | ||||
| 
 | ||||
|         var changed = tick(stack, entity, computer); | ||||
|         if (changed && inventory != null) inventory.setChanged(); | ||||
|         // If we're in the inventory, create a computer and keep it alive. | ||||
|         var holder = new PocketHolder.PlayerHolder(player, slotNum); | ||||
|         var brain = getOrCreateBrain((ServerLevel) world, holder, stack); | ||||
|         brain.computer().keepAlive(); | ||||
| 
 | ||||
|         tick(stack, holder, brain); | ||||
|     } | ||||
| 
 | ||||
|     @ForgeOverride | ||||
| @@ -95,8 +119,11 @@ public class PocketComputerItem extends Item implements IMedia { | ||||
|         var level = entity.level(); | ||||
|         if (level.isClientSide || level.getServer() == null) return false; | ||||
| 
 | ||||
|         // If we're an item entity, tick an already existing computer (as to update the position), but do not keep the | ||||
|         // computer alive. | ||||
|         var computer = getServerComputer(level.getServer(), stack); | ||||
|         if (computer != null && tick(stack, entity, computer)) entity.setItem(stack.copy()); | ||||
|         if (computer != null) tick(stack, new PocketHolder.ItemEntityHolder(entity), computer.getBrain()); | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| @@ -104,14 +131,18 @@ public class PocketComputerItem extends Item implements IMedia { | ||||
|     public InteractionResultHolder<ItemStack> use(Level world, Player player, InteractionHand hand) { | ||||
|         var stack = player.getItemInHand(hand); | ||||
|         if (!world.isClientSide) { | ||||
|             var computer = createServerComputer((ServerLevel) world, player, player.getInventory(), stack); | ||||
|             var holder = new PocketHolder.PlayerHolder((ServerPlayer) player, InventoryUtil.getHandSlot(player, hand)); | ||||
|             var brain = getOrCreateBrain((ServerLevel) world, holder, stack); | ||||
|             var computer = brain.computer(); | ||||
|             computer.turnOn(); | ||||
| 
 | ||||
|             var stop = false; | ||||
|             var upgrade = getUpgrade(stack); | ||||
|             if (upgrade != null) { | ||||
|                 computer.updateValues(player, stack, upgrade); | ||||
|                 stop = upgrade.onRightClick(world, computer, computer.getPeripheral(ComputerSide.BACK)); | ||||
|                 brain.updateHolder(holder); | ||||
|                 stop = upgrade.onRightClick(world, brain, computer.getPeripheral(ComputerSide.BACK)); | ||||
|                 // Sync back just in case. We don't need to setChanged, as we'll return the item anyway. | ||||
|                 updateItem(stack, brain); | ||||
|             } | ||||
| 
 | ||||
|             if (!stop) { | ||||
| @@ -153,34 +184,42 @@ public class PocketComputerItem extends Item implements IMedia { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) { | ||||
| 
 | ||||
|     private PocketBrain getOrCreateBrain(ServerLevel level, PocketHolder holder, ItemStack stack) { | ||||
|         var registry = ServerContext.get(level.getServer()).registry(); | ||||
|         var computer = (PocketServerComputer) ServerComputerReference.get(stack, registry); | ||||
|         if (computer == null) { | ||||
|             var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER); | ||||
|             computer = new PocketServerComputer(level, entity.blockPosition(), computerID, getLabel(stack), getFamily()); | ||||
| 
 | ||||
|             var instanceId = computer.register(); | ||||
|             stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), instanceId)); | ||||
| 
 | ||||
|             var upgrade = getUpgrade(stack); | ||||
| 
 | ||||
|             computer.updateValues(entity, stack, upgrade); | ||||
|             computer.addAPI(new PocketAPI(computer)); | ||||
| 
 | ||||
|             // Only turn on when initially creating the computer, rather than each tick. | ||||
|             if (isMarkedOn(stack) && entity instanceof Player) computer.turnOn(); | ||||
| 
 | ||||
|             if (inventory != null) inventory.setChanged(); | ||||
|         { | ||||
|             var computer = getServerComputer(registry, stack); | ||||
|             if (computer != null) return computer.getBrain(); | ||||
|         } | ||||
| 
 | ||||
|         return computer; | ||||
|         var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER); | ||||
|         var brain = new PocketBrain(holder, computerID, getLabel(stack), getFamily(), getUpgradeWithData(stack)); | ||||
|         var computer = brain.computer(); | ||||
| 
 | ||||
|         stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register())); | ||||
| 
 | ||||
|         // Only turn on when initially creating the computer, rather than each tick. | ||||
|         if (isMarkedOn(stack) && holder instanceof PocketHolder.PlayerHolder) computer.turnOn(); | ||||
| 
 | ||||
|         updateItem(stack, brain); | ||||
| 
 | ||||
|         holder.setChanged(); | ||||
| 
 | ||||
|         return brain; | ||||
|     } | ||||
| 
 | ||||
|     public static boolean isServerComputer(ServerComputer computer, ItemStack stack) { | ||||
|         return stack.getItem() instanceof PocketComputerItem | ||||
|             && getServerComputer(computer.getLevel().getServer(), stack) == computer; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public static PocketServerComputer getServerComputer(ServerComputerRegistry registry, ItemStack stack) { | ||||
|         return (PocketServerComputer) ServerComputerReference.get(stack, registry); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public static PocketServerComputer getServerComputer(MinecraftServer server, ItemStack stack) { | ||||
|         return (PocketServerComputer) ServerComputerReference.get(stack, ServerContext.get(server).registry()); | ||||
|         return getServerComputer(ServerContext.get(server).registry(), stack); | ||||
|     } | ||||
| 
 | ||||
|     public ComputerFamily getFamily() { | ||||
|   | ||||
| @@ -41,8 +41,6 @@ public class PocketModem extends AbstractPocketUpgrade { | ||||
|     public void update(IPocketAccess access, @Nullable IPeripheral peripheral) { | ||||
|         if (!(peripheral instanceof PocketModemPeripheral modem)) return; | ||||
| 
 | ||||
|         modem.setLocation(access); | ||||
| 
 | ||||
|         var state = modem.getModemState(); | ||||
|         if (state.pollChanged()) access.setLight(state.isOpen() ? 0xBA0000 : -1); | ||||
|     } | ||||
|   | ||||
| @@ -14,31 +14,21 @@ import net.minecraft.world.phys.Vec3; | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| public class PocketModemPeripheral extends WirelessModemPeripheral { | ||||
|     private @Nullable Level level = null; | ||||
|     private Vec3 position = Vec3.ZERO; | ||||
|     private final IPocketAccess access; | ||||
| 
 | ||||
|     public PocketModemPeripheral(boolean advanced, IPocketAccess access) { | ||||
|         super(new ModemState(), advanced); | ||||
|         setLocation(access); | ||||
|     } | ||||
| 
 | ||||
|     void setLocation(IPocketAccess access) { | ||||
|         var entity = access.getEntity(); | ||||
|         if (entity != null) { | ||||
|             level = entity.level(); | ||||
|             position = entity.getEyePosition(1); | ||||
|         } | ||||
|         this.access = access; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Level getLevel() { | ||||
|         if (level == null) throw new IllegalStateException("Using modem before position has been defined"); | ||||
|         return level; | ||||
|         return access.getLevel(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Vec3 getPosition() { | ||||
|         return position; | ||||
|         return access.getPosition(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|   | ||||
| @@ -8,15 +8,11 @@ import dan200.computercraft.api.peripheral.IPeripheral; | ||||
| import dan200.computercraft.api.pocket.IPocketAccess; | ||||
| import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; | ||||
| import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral; | ||||
| import net.minecraft.world.level.Level; | ||||
| import net.minecraft.world.phys.Vec3; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral { | ||||
|     private final IPocketAccess access; | ||||
|     private @Nullable Level level; | ||||
|     private Vec3 position = Vec3.ZERO; | ||||
| 
 | ||||
|     public PocketSpeakerPeripheral(IPocketAccess access) { | ||||
|         this.access = access; | ||||
| @@ -25,7 +21,7 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral { | ||||
|     @Override | ||||
|     public SpeakerPosition getPosition() { | ||||
|         var entity = access.getEntity(); | ||||
|         return entity == null ? SpeakerPosition.of(level, position) : SpeakerPosition.of(entity); | ||||
|         return entity == null ? SpeakerPosition.of(access.getLevel(), access.getPosition()) : SpeakerPosition.of(entity); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -35,12 +31,6 @@ public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral { | ||||
| 
 | ||||
|     @Override | ||||
|     public void update() { | ||||
|         var entity = access.getEntity(); | ||||
|         if (entity != null) { | ||||
|             level = entity.level(); | ||||
|             position = entity.position(); | ||||
|         } | ||||
| 
 | ||||
|         super.update(); | ||||
| 
 | ||||
|         access.setLight(madeSound() ? 0x3320fc : -1); | ||||
|   | ||||
| @@ -11,7 +11,6 @@ import dan200.computercraft.api.turtle.TurtleCommandResult; | ||||
| import dan200.computercraft.api.turtle.TurtleSide; | ||||
| import dan200.computercraft.core.metrics.Metrics; | ||||
| import dan200.computercraft.core.metrics.MetricsObserver; | ||||
| import dan200.computercraft.shared.computer.core.ServerComputer; | ||||
| import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods; | ||||
| import dan200.computercraft.shared.turtle.core.*; | ||||
| 
 | ||||
| @@ -68,8 +67,8 @@ public class TurtleAPI implements ILuaAPI { | ||||
|     private final MetricsObserver metrics; | ||||
|     private final TurtleAccessInternal turtle; | ||||
| 
 | ||||
|     public TurtleAPI(ServerComputer computer, TurtleAccessInternal turtle) { | ||||
|         this.metrics = computer.getMetrics(); | ||||
|     public TurtleAPI(MetricsObserver metrics, TurtleAccessInternal turtle) { | ||||
|         this.metrics = metrics; | ||||
|         this.turtle = turtle; | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| package dan200.computercraft.shared.turtle.blocks; | ||||
| 
 | ||||
| import com.mojang.authlib.GameProfile; | ||||
| import dan200.computercraft.api.component.ComputerComponents; | ||||
| import dan200.computercraft.api.peripheral.IPeripheral; | ||||
| import dan200.computercraft.api.turtle.ITurtleAccess; | ||||
| import dan200.computercraft.api.turtle.ITurtleUpgrade; | ||||
| @@ -21,9 +22,9 @@ import dan200.computercraft.shared.config.Config; | ||||
| import dan200.computercraft.shared.container.BasicContainer; | ||||
| import dan200.computercraft.shared.platform.PlatformHelper; | ||||
| import dan200.computercraft.shared.turtle.TurtleOverlay; | ||||
| import dan200.computercraft.shared.turtle.apis.TurtleAPI; | ||||
| import dan200.computercraft.shared.turtle.core.TurtleBrain; | ||||
| import dan200.computercraft.shared.turtle.inventory.TurtleMenu; | ||||
| import dan200.computercraft.shared.util.ComponentMap; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.core.Direction; | ||||
| import net.minecraft.core.HolderLookup; | ||||
| @@ -81,10 +82,9 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba | ||||
|     protected ServerComputer createComputer(int id) { | ||||
|         var computer = new ServerComputer( | ||||
|             (ServerLevel) getLevel(), getBlockPos(), id, label, | ||||
|             getFamily(), Config.turtleTermWidth, | ||||
|             Config.turtleTermHeight | ||||
|             getFamily(), Config.turtleTermWidth, Config.turtleTermHeight, | ||||
|             ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build() | ||||
|         ); | ||||
|         computer.addAPI(new TurtleAPI(computer, brain)); | ||||
|         brain.setupComputer(computer); | ||||
|         return computer; | ||||
|     } | ||||
|   | ||||
| @@ -0,0 +1,66 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.util; | ||||
| 
 | ||||
| import dan200.computercraft.api.component.ComputerComponent; | ||||
| import dan200.computercraft.core.metrics.MetricsObserver; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * An immutable map of components. | ||||
|  */ | ||||
| public final class ComponentMap { | ||||
|     public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics"); | ||||
| 
 | ||||
|     private static final ComponentMap EMPTY = new ComponentMap(Map.of()); | ||||
| 
 | ||||
|     private final Map<ComputerComponent<?>, Object> components; | ||||
| 
 | ||||
|     private ComponentMap(Map<ComputerComponent<?>, Object> components) { | ||||
|         this.components = components; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public <T> @Nullable T get(ComputerComponent<T> component) { | ||||
|         return (T) components.get(component); | ||||
|     } | ||||
| 
 | ||||
|     public static ComponentMap empty() { | ||||
|         return EMPTY; | ||||
|     } | ||||
| 
 | ||||
|     public static Builder builder() { | ||||
|         return new Builder(); | ||||
|     } | ||||
| 
 | ||||
|     public static final class Builder { | ||||
|         private final Map<ComputerComponent<?>, Object> components = new HashMap<>(); | ||||
| 
 | ||||
|         private Builder() { | ||||
|         } | ||||
| 
 | ||||
|         public <T> Builder add(ComputerComponent<T> component, T value) { | ||||
|             addImpl(component, value); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder add(ComponentMap components) { | ||||
|             for (var component : components.components.entrySet()) addImpl(component.getKey(), component.getValue()); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         private void addImpl(ComputerComponent<?> component, Object value) { | ||||
|             if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set"); | ||||
|             components.put(component, value); | ||||
|         } | ||||
| 
 | ||||
|         public ComponentMap build() { | ||||
|             return new ComponentMap(Map.copyOf(components)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -8,6 +8,9 @@ import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.core.Direction; | ||||
| import net.minecraft.server.level.ServerLevel; | ||||
| import net.minecraft.world.Container; | ||||
| import net.minecraft.world.InteractionHand; | ||||
| import net.minecraft.world.entity.player.Inventory; | ||||
| import net.minecraft.world.entity.player.Player; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| import net.minecraft.world.phys.EntityHitResult; | ||||
| import net.minecraft.world.phys.Vec3; | ||||
| @@ -18,6 +21,20 @@ public final class InventoryUtil { | ||||
|     private InventoryUtil() { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the inventory slot for a given hand. | ||||
|      * | ||||
|      * @param player The player to get the slot from. | ||||
|      * @param hand   The hand to get. | ||||
|      * @return The current slot. | ||||
|      */ | ||||
|     public static int getHandSlot(Player player, InteractionHand hand) { | ||||
|         return switch (hand) { | ||||
|             case MAIN_HAND -> player.getInventory().selected; | ||||
|             case OFF_HAND -> Inventory.SLOT_OFFHAND; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public static @Nullable Container getEntityContainer(ServerLevel level, BlockPos pos, Direction side) { | ||||
|         var vecStart = new Vec3( | ||||
|             pos.getX() + 0.5 + 0.6 * side.getStepX(), | ||||
|   | ||||
| @@ -29,6 +29,7 @@ import dan200.computercraft.shared.util.WaterloggableHelpers | ||||
| import dan200.computercraft.test.core.assertArrayEquals | ||||
| import dan200.computercraft.test.core.computer.LuaTaskContext | ||||
| import dan200.computercraft.test.core.computer.getApi | ||||
| import dan200.computercraft.test.shared.ItemStackMatcher.isStack | ||||
| import net.minecraft.core.BlockPos | ||||
| import net.minecraft.core.registries.Registries | ||||
| import net.minecraft.gametest.framework.GameTest | ||||
| @@ -44,8 +45,7 @@ import net.minecraft.world.level.block.FenceBlock | ||||
| import net.minecraft.world.level.block.entity.BlockEntityType | ||||
| import net.minecraft.world.level.block.state.properties.BlockStateProperties | ||||
| import org.hamcrest.MatcherAssert.assertThat | ||||
| import org.hamcrest.Matchers.array | ||||
| import org.hamcrest.Matchers.instanceOf | ||||
| import org.hamcrest.Matchers.* | ||||
| import org.junit.jupiter.api.Assertions.assertEquals | ||||
| import org.junit.jupiter.api.Assertions.assertNotEquals | ||||
| import java.util.* | ||||
| @@ -693,6 +693,47 @@ class Turtle_Test { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * `turtle.craft` works as expected | ||||
|      */ | ||||
|     @GameTest | ||||
|     fun Craft(helper: GameTestHelper) = helper.sequence { | ||||
|         thenOnComputer { | ||||
|             callPeripheral("left", "craft", 1).assertArrayEquals(true) | ||||
|         } | ||||
|         thenExecute { | ||||
|             val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get()) | ||||
|             assertThat( | ||||
|                 "Inventory is as expected.", | ||||
|                 turtle.items, | ||||
|                 contains( | ||||
|                     isStack(Items.DIAMOND, 1), isStack(Items.DIAMOND, 1), isStack(Items.DIAMOND, 1), isStack(Items.DIAMOND_PICKAXE, 1), | ||||
|                     isStack(ItemStack.EMPTY), isStack(Items.STICK, 1), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), | ||||
|                     isStack(ItemStack.EMPTY), isStack(Items.STICK, 1), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), | ||||
|                     isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), isStack(ItemStack.EMPTY), | ||||
|                 ), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * `turtle.equipLeft` equips a tool. | ||||
|      */ | ||||
|     @GameTest | ||||
|     fun Equip_tool(helper: GameTestHelper) = helper.sequence { | ||||
|         thenOnComputer { | ||||
|             turtle.equipLeft().await().assertArrayEquals(true) | ||||
|         } | ||||
|         thenExecute { | ||||
|             val turtle = helper.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.TURTLE_NORMAL.get()) | ||||
|             assertEquals( | ||||
|                 helper.level.registryAccess().registryOrThrow(ITurtleUpgrade.REGISTRY) | ||||
|                     .get(ResourceLocation.withDefaultNamespace("diamond_pickaxe")), | ||||
|                 turtle.getUpgrade(TurtleSide.LEFT), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Render turtles as an item. | ||||
|      */ | ||||
|   | ||||
| @@ -179,7 +179,6 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P | ||||
| fun GameTestHelper.getContainerAt(pos: BlockPos): Container = | ||||
|     when (val container: BlockEntity = 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) | ||||
|     } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										137
									
								
								projects/common/src/testMod/resources/data/cctest/structures/turtle_test.craft.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								projects/common/src/testMod/resources/data/cctest/structures/turtle_test.craft.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| { | ||||
|     DataVersion: 3465, | ||||
|     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: "minecraft:air"}, | ||||
|         {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:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 2b, Slot: 0b, id: "minecraft:diamond"}, {Count: 2b, Slot: 1b, id: "minecraft:diamond"}, {Count: 2b, Slot: 2b, id: "minecraft:diamond"}, {Count: 2b, Slot: 5b, id: "minecraft:stick"}, {Count: 2b, Slot: 9b, id: "minecraft:stick"}], Label: "turtle_test.craft", LeftUpgrade: "minecraft:crafting_table", LeftUpgradeNbt: {}, On: 1b, Slot: 0, id: "computercraft:turtle_normal"}}, | ||||
|         {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:turtle_normal{facing:north,waterlogged:false}" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										137
									
								
								projects/common/src/testMod/resources/data/cctest/structures/turtle_test.equip_tool.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								projects/common/src/testMod/resources/data/cctest/structures/turtle_test.equip_tool.snbt
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| { | ||||
|     DataVersion: 3465, | ||||
|     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: "minecraft:air"}, | ||||
|         {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:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [{Count: 1b, Slot: 0b, id: "minecraft:diamond_pickaxe"}], Label: "turtle_test.equip_tool", On: 1b, Slot: 0, id: "computercraft:turtle_normal"}}, | ||||
|         {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:turtle_normal{facing:north,waterlogged:false}" | ||||
|     ] | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| // SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.api.lua; | ||||
| 
 | ||||
| import dan200.computercraft.api.peripheral.IComputerAccess; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * An interface passed to {@link ILuaAPIFactory} in order to provide additional information | ||||
|  * about a computer. | ||||
|  */ | ||||
| public interface IComputerSystem extends IComputerAccess { | ||||
|     /** | ||||
|      * Get the label for this computer. | ||||
|      * | ||||
|      * @return This computer's label, or {@code null} if it is not set. | ||||
|      */ | ||||
|     @Nullable | ||||
|     String getLabel(); | ||||
| } | ||||
| @@ -4,7 +4,6 @@ | ||||
| 
 | ||||
| package dan200.computercraft.core; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.ILuaAPIFactory; | ||||
| import dan200.computercraft.core.asm.GenericMethod; | ||||
| import dan200.computercraft.core.asm.LuaMethodSupplier; | ||||
| import dan200.computercraft.core.asm.PeripheralMethodSupplier; | ||||
| @@ -35,21 +34,19 @@ public final class ComputerContext { | ||||
|     private final ComputerScheduler computerScheduler; | ||||
|     private final MainThreadScheduler mainThreadScheduler; | ||||
|     private final ILuaMachine.Factory luaFactory; | ||||
|     private final List<ILuaAPIFactory> apiFactories; | ||||
|     private final MethodSupplier<LuaMethod> luaMethods; | ||||
|     private final MethodSupplier<PeripheralMethod> peripheralMethods; | ||||
| 
 | ||||
|     ComputerContext( | ||||
|     private ComputerContext( | ||||
|         GlobalEnvironment globalEnvironment, ComputerScheduler computerScheduler, | ||||
|         MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory luaFactory, | ||||
|         List<ILuaAPIFactory> apiFactories, MethodSupplier<LuaMethod> luaMethods, | ||||
|         MethodSupplier<LuaMethod> luaMethods, | ||||
|         MethodSupplier<PeripheralMethod> peripheralMethods | ||||
|     ) { | ||||
|         this.globalEnvironment = globalEnvironment; | ||||
|         this.computerScheduler = computerScheduler; | ||||
|         this.mainThreadScheduler = mainThreadScheduler; | ||||
|         this.luaFactory = luaFactory; | ||||
|         this.apiFactories = apiFactories; | ||||
|         this.luaMethods = luaMethods; | ||||
|         this.peripheralMethods = peripheralMethods; | ||||
|     } | ||||
| @@ -91,15 +88,6 @@ public final class ComputerContext { | ||||
|         return luaFactory; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Additional APIs to inject into each computer. | ||||
|      * | ||||
|      * @return All available API factories. | ||||
|      */ | ||||
|     public List<ILuaAPIFactory> apiFactories() { | ||||
|         return apiFactories; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the {@link MethodSupplier} used to find methods on Lua values. | ||||
|      * | ||||
| @@ -166,7 +154,6 @@ public final class ComputerContext { | ||||
|         private @Nullable ComputerScheduler computerScheduler = null; | ||||
|         private @Nullable MainThreadScheduler mainThreadScheduler; | ||||
|         private @Nullable ILuaMachine.Factory luaFactory; | ||||
|         private @Nullable List<ILuaAPIFactory> apiFactories; | ||||
|         private @Nullable List<GenericMethod> genericMethods; | ||||
| 
 | ||||
|         Builder(GlobalEnvironment environment) { | ||||
| @@ -227,20 +214,6 @@ public final class ComputerContext { | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Set the additional {@linkplain ILuaAPIFactory APIs} to add to each computer. | ||||
|          * | ||||
|          * @param apis A list of API factories. | ||||
|          * @return {@code this}, for chaining | ||||
|          * @see ComputerContext#apiFactories() | ||||
|          */ | ||||
|         public Builder apiFactories(Collection<ILuaAPIFactory> apis) { | ||||
|             Objects.requireNonNull(apis); | ||||
|             if (apiFactories != null) throw new IllegalStateException("Main-thread scheduler already specified"); | ||||
|             apiFactories = List.copyOf(apis); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Set the set of {@link GenericMethod}s used by the {@linkplain MethodSupplier method suppliers}. | ||||
|          * | ||||
| @@ -267,7 +240,6 @@ public final class ComputerContext { | ||||
|                 computerScheduler == null ? new ComputerThread(1) : computerScheduler, | ||||
|                 mainThreadScheduler == null ? new NoWorkMainThreadScheduler() : mainThreadScheduler, | ||||
|                 luaFactory == null ? CobaltLuaMachine::new : luaFactory, | ||||
|                 apiFactories == null ? List.of() : apiFactories, | ||||
|                 LuaMethodSupplier.create(genericMethods == null ? List.of() : genericMethods), | ||||
|                 PeripheralMethodSupplier.create(genericMethods == null ? List.of() : genericMethods) | ||||
|             ); | ||||
|   | ||||
| @@ -21,7 +21,7 @@ public abstract class ComputerAccess implements IComputerAccess { | ||||
|     private static final Logger LOG = LoggerFactory.getLogger(ComputerAccess.class); | ||||
| 
 | ||||
|     private final IAPIEnvironment environment; | ||||
|     private final Set<String> mounts = new HashSet<>(); | ||||
|     private final Set<String> mounts = new HashSet<>(0); | ||||
| 
 | ||||
|     protected ComputerAccess(IAPIEnvironment environment) { | ||||
|         this.environment = environment; | ||||
|   | ||||
| @@ -0,0 +1,28 @@ | ||||
| // SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.core.computer; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| 
 | ||||
| /** | ||||
|  * Hooks for managing the lifecycle of an API. This allows adding additional logic to an API's {@link ILuaAPI#startup()} | ||||
|  * and {@link ILuaAPI#shutdown()} methods. | ||||
|  * | ||||
|  * @see ILuaAPI | ||||
|  * @see Computer#addApi(ILuaAPI, ApiLifecycle) | ||||
|  */ | ||||
| public interface ApiLifecycle { | ||||
|     /** | ||||
|      * Called before the API's {@link ILuaAPI#startup()} method, may be used to set up resources. | ||||
|      */ | ||||
|     default void startup() { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called after the API's {@link ILuaAPI#shutdown()} method, may be used to tear down resources. | ||||
|      */ | ||||
|     default void shutdown() { | ||||
|     } | ||||
| } | ||||
| @@ -9,18 +9,14 @@ import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import javax.annotation.Nullable; | ||||
| 
 | ||||
| /** | ||||
|  * A wrapper for {@link ILuaAPI}s which optionally manages the lifecycle of a {@link ComputerSystem}. | ||||
|  * A wrapper for {@link ILuaAPI}s which provides an optional shutdown hook to clean up resources. | ||||
|  * | ||||
|  * @param api       The original API. | ||||
|  * @param lifecycle The optional lifecycle hooks for this API. | ||||
|  */ | ||||
| final class ApiWrapper { | ||||
|     private final ILuaAPI api; | ||||
|     private final @Nullable ComputerSystem system; | ||||
| 
 | ||||
|     ApiWrapper(ILuaAPI api, @Nullable ComputerSystem system) { | ||||
|         this.api = api; | ||||
|         this.system = system; | ||||
|     } | ||||
| 
 | ||||
| record ApiWrapper(ILuaAPI api, @Nullable ApiLifecycle lifecycle) { | ||||
|     public void startup() { | ||||
|         if (lifecycle != null) lifecycle.startup(); | ||||
|         api.startup(); | ||||
|     } | ||||
| 
 | ||||
| @@ -30,7 +26,7 @@ final class ApiWrapper { | ||||
| 
 | ||||
|     public void shutdown() { | ||||
|         api.shutdown(); | ||||
|         if (system != null) system.unmountAll(); | ||||
|         if (lifecycle != null) lifecycle.shutdown(); | ||||
|     } | ||||
| 
 | ||||
|     public ILuaAPI api() { | ||||
|   | ||||
| @@ -184,6 +184,10 @@ public class Computer { | ||||
|         executor.addApi(api); | ||||
|     } | ||||
| 
 | ||||
|     public void addApi(ILuaAPI api, ApiLifecycle lifecycleHooks) { | ||||
|         executor.addApi(api, lifecycleHooks); | ||||
|     } | ||||
| 
 | ||||
|     long getUniqueTaskId() { | ||||
|         return lastTaskId.incrementAndGet(); | ||||
|     } | ||||
|   | ||||
| @@ -162,13 +162,6 @@ final class ComputerExecutor implements ComputerScheduler.Worker { | ||||
|         addApi(new PeripheralAPI(environment, context.peripheralMethods())); | ||||
|         addApi(new OSAPI(environment)); | ||||
|         if (CoreConfig.httpEnabled) addApi(new HTTPAPI(environment)); | ||||
| 
 | ||||
|         // Load in the externally registered APIs. | ||||
|         for (var factory : context.apiFactories()) { | ||||
|             var system = new ComputerSystem(environment); | ||||
|             var api = factory.create(system); | ||||
|             if (api != null) apis.add(new ApiWrapper(api, system)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -190,6 +183,10 @@ final class ComputerExecutor implements ComputerScheduler.Worker { | ||||
|         apis.add(new ApiWrapper(api, null)); | ||||
|     } | ||||
| 
 | ||||
|     void addApi(ILuaAPI api, ApiLifecycle lifecycleHooks) { | ||||
|         apis.add(new ApiWrapper(api, lifecycleHooks)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Schedule this computer to be started if not already on. | ||||
|      */ | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| // SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.core.computer; | ||||
| 
 | ||||
| import dan200.computercraft.api.lua.IComputerSystem; | ||||
| import dan200.computercraft.api.lua.ILuaAPIFactory; | ||||
| import dan200.computercraft.api.peripheral.IComputerAccess; | ||||
| import dan200.computercraft.api.peripheral.IPeripheral; | ||||
| import dan200.computercraft.core.apis.ComputerAccess; | ||||
| import dan200.computercraft.core.apis.IAPIEnvironment; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * Implementation of {@link IComputerAccess}/{@link IComputerSystem} for usage by externally registered APIs. | ||||
|  * | ||||
|  * @see ILuaAPIFactory | ||||
|  * @see ApiWrapper | ||||
|  */ | ||||
| public class ComputerSystem extends ComputerAccess implements IComputerSystem { | ||||
|     private final IAPIEnvironment environment; | ||||
| 
 | ||||
|     ComputerSystem(IAPIEnvironment environment) { | ||||
|         super(environment); | ||||
|         this.environment = environment; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getAttachmentName() { | ||||
|         return "computer"; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public String getLabel() { | ||||
|         return environment.getLabel(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Map<String, IPeripheral> getAvailablePeripherals() { | ||||
|         // TODO: Should this return peripherals on the current computer? | ||||
|         return Map.of(); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public IPeripheral getAvailablePeripheral(String name) { | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -7,9 +7,13 @@ | ||||
| -- @module textutils | ||||
| -- @since 1.2 | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua") | ||||
| local pgk_env = setmetatable({}, { __index = _ENV }) | ||||
| pgk_env.require = dofile("rom/modules/main/cc/require.lua").make(pgk_env, "rom/modules/main") | ||||
| local require = pgk_env.require | ||||
|  | ||||
| local expect = require("cc.expect") | ||||
| local expect, field = expect.expect, expect.field | ||||
| local wrap = dofile("rom/modules/main/cc/strings.lua").wrap | ||||
| local wrap = require("cc.strings").wrap | ||||
|  | ||||
| --- Slowly writes string text at current cursor position, | ||||
| -- character-by-character. | ||||
|   | ||||
| @@ -1,3 +1,16 @@ | ||||
| # New features in CC: Tweaked 1.112.0 | ||||
| 
 | ||||
| * Report a custom error when using `!` instead of `not`. | ||||
| * Update several translations (zyxkad, MineKID-LP). | ||||
| * Add `cc.strings.split` function. | ||||
| 
 | ||||
| Several bug fixes: | ||||
| * Fix `drive.getAudioTitle` returning `nil` when no disk is inserted. | ||||
| * Preserve item data when upgrading pocket computers. | ||||
| * Add missing bounds check to `cc.strings.wrap` (Lupus950). | ||||
| * Fix dyed turtles rendering transparent. | ||||
| * Fix dupe bug when crafting with turtles. | ||||
| 
 | ||||
| # New features in CC: Tweaked 1.111.1 | ||||
| 
 | ||||
| * Add support for data-driven turtle upgrades. | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| New features in CC: Tweaked 1.111.1 | ||||
| New features in CC: Tweaked 1.112.0 | ||||
| 
 | ||||
| * Add support for data-driven turtle upgrades. | ||||
| * Report a custom error when using `!` instead of `not`. | ||||
| * Update several translations (zyxkad, MineKID-LP). | ||||
| * Add `cc.strings.split` function. | ||||
| 
 | ||||
| Several bug fixes: | ||||
| * Fix monitors not rendering on NeoForge. | ||||
| * Fix turtle labels not rendering. | ||||
| * Fix compatibility with newer versions of NeoForge. | ||||
| * Fix heights of turtle flags. | ||||
| * Fix `drive.getAudioTitle` returning `nil` when no disk is inserted. | ||||
| * Preserve item data when upgrading pocket computers. | ||||
| * Add missing bounds check to `cc.strings.wrap` (Lupus950). | ||||
| * Fix dyed turtles rendering transparent. | ||||
| * Fix dupe bug when crafting with turtles. | ||||
| 
 | ||||
| Type "help changelog" to see the full version history. | ||||
|   | ||||
| @@ -118,8 +118,8 @@ end | ||||
| --- Expect a number to be within a specific range. | ||||
| -- | ||||
| -- @tparam number num The value to check. | ||||
| -- @tparam number min The minimum value, if nil then `-math.huge` is used. | ||||
| -- @tparam number max The maximum value, if nil then `math.huge` is used. | ||||
| -- @tparam[opt=-math.huge] number min The minimum value. | ||||
| -- @tparam[opt=math.huge] number max The maximum value. | ||||
| -- @return The given `value`. | ||||
| -- @throws If the value is outside of the allowed range. | ||||
| -- @since 1.96.0 | ||||
|   | ||||
| @@ -8,7 +8,8 @@ | ||||
| -- @since 1.95.0 | ||||
| -- @see textutils For additional string related utilities. | ||||
|  | ||||
| local expect = (require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")).expect | ||||
| local expect = require("cc.expect") | ||||
| local expect, range = expect.expect, expect.range | ||||
|  | ||||
| --[[- Wraps a block of text, so that each line fits within the given width. | ||||
|  | ||||
| @@ -32,7 +33,7 @@ local function wrap(text, width) | ||||
|     expect(1, text, "string") | ||||
|     expect(2, width, "number", "nil") | ||||
|     width = width or term.getSize() | ||||
|  | ||||
|     range(width, 1) | ||||
|  | ||||
|     local lines, lines_n, current_line = {}, 0, "" | ||||
|     local function push_line() | ||||
| @@ -109,7 +110,63 @@ local function ensure_width(line, width) | ||||
|     return line | ||||
| end | ||||
|  | ||||
| --[[- Split a string into parts, each separated by a deliminator. | ||||
|  | ||||
| For instance, splitting the string `"a b c"` with the deliminator `" "`, would | ||||
| return a table with three strings: `"a"`, `"b"`, and `"c"`. | ||||
|  | ||||
| By default, the deliminator is given as a [Lua pattern][pattern]. Passing `true` | ||||
| to the `plain` argument will cause the deliminator to be treated as a litteral | ||||
| string. | ||||
|  | ||||
| [pattern]: https://www.lua.org/manual/5.3/manual.html#6.4.1 | ||||
|  | ||||
| @tparam string str The string to split. | ||||
| @tparam string deliminator The pattern to split this string on. | ||||
| @tparam[opt=false] boolean plain Treat the deliminator as a plain string, rather than a pattern. | ||||
| @tparam[opt] number limit The maximum number of elements in the returned list. | ||||
| @treturn { string... } The list of split strings. | ||||
|  | ||||
| @usage Split a string into words. | ||||
|  | ||||
|     require "cc.strings".split("This is a sentence.", "%s+") | ||||
|  | ||||
| @usage Split a string by "-" into at most 3 elements. | ||||
|  | ||||
|     require "cc.strings".split("a-separated-string-of-sorts", "-", true, 3) | ||||
|  | ||||
| @see table.concat To join strings together. | ||||
|  | ||||
| @since 1.112.0 | ||||
| ]] | ||||
| local function split(str, deliminator, plain, limit) | ||||
|     expect(1, str, "string") | ||||
|     expect(2, deliminator, "string") | ||||
|     expect(3, plain, "boolean", "nil") | ||||
|     expect(4, limit, "number", "nil") | ||||
|  | ||||
|     local out, out_n, pos = {}, 0, 1 | ||||
|     while not limit or out_n < limit - 1 do | ||||
|         local start, finish = str:find(deliminator, pos, plain) | ||||
|         if not start then break end | ||||
|  | ||||
|         out_n = out_n + 1 | ||||
|         out[out_n] = str:sub(pos, start - 1) | ||||
|  | ||||
|         -- Require us to advance by at least one character. | ||||
|         if finish < start then error("separator is empty", 2) end | ||||
|  | ||||
|         pos = finish + 1 | ||||
|     end | ||||
|  | ||||
|     if pos == 1 then return { str } end | ||||
|  | ||||
|     out[out_n + 1] = str:sub(pos) | ||||
|     return out | ||||
| end | ||||
|  | ||||
| return { | ||||
|     wrap = wrap, | ||||
|     ensure_width = ensure_width, | ||||
|     split = split, | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| -- | ||||
| -- SPDX-License-Identifier: MPL-2.0 | ||||
|  | ||||
| describe("cc.pretty", function() | ||||
| describe("cc.strings", function() | ||||
|     local str = require("cc.strings") | ||||
|  | ||||
|     describe("wrap", function() | ||||
| @@ -11,6 +11,8 @@ describe("cc.pretty", function() | ||||
|             str.wrap("test string is long", 11) | ||||
|             expect.error(str.wrap, nil):eq("bad argument #1 (string expected, got nil)") | ||||
|             expect.error(str.wrap, "", false):eq("bad argument #2 (number expected, got boolean)") | ||||
|  | ||||
|             expect.error(str.wrap, "", 0):eq("number outside of range (expected 0 to be within 1 and inf)") | ||||
|         end) | ||||
|  | ||||
|         it("wraps lines", function() | ||||
| @@ -42,4 +44,33 @@ describe("cc.pretty", function() | ||||
|             expect(str.ensure_width("test string is long", 15)):eq("test string is ") | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("split", function() | ||||
|         it("splits with empty segments", function() | ||||
|             expect(str.split("", "%-")):same { "" } | ||||
|             expect(str.split("-", "%-")):same { "", "" } | ||||
|             expect(str.split("---", "%-")):same { "", "", "", "" } | ||||
|             expect(str.split("-a", "%-")):same { "", "a" } | ||||
|             expect(str.split("a-", "%-")):same { "a", "" } | ||||
|         end) | ||||
|  | ||||
|         it("cannot split with an empty separator", function() | ||||
|             expect.error(str.split, "abc", ""):eq("separator is empty") | ||||
|         end) | ||||
|  | ||||
|         it("splits on patterns", function() | ||||
|             expect(str.split("a.bcd      ef", "%W+")):same { "a", "bcd", "ef" } | ||||
|         end) | ||||
|  | ||||
|         it("splits on literal strings", function() | ||||
|             expect(str.split("a-bcd-ef", "-", true)):same { "a", "bcd", "ef" } | ||||
|         end) | ||||
|  | ||||
|         it("accepts a limit", function() | ||||
|             expect(str.split("foo-bar-baz-qux-quyux", "-", true, 3)):same { "foo", "bar", "baz-qux-quyux" } | ||||
|             expect(str.split("foo-bar-baz", "-", true, 5)):same { "foo", "bar", "baz" } | ||||
|             expect(str.split("foo-bar-baz", "-", true, 1)):same { "foo-bar-baz" } | ||||
|             expect(str.split("foo-bar-baz", "-", true, 1)):same { "foo-bar-baz" } | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -69,6 +69,7 @@ dependencies { | ||||
|         exclude("net.fabricmc", "fabric-loader") | ||||
|         exclude("net.fabricmc.fabric-api") | ||||
|     } | ||||
|     modCompileOnly(libs.create.fabric) { isTransitive = false } | ||||
| 
 | ||||
|     modClientRuntimeOnly(libs.bundles.externalMods.fabric.runtime) { | ||||
|         exclude("net.fabricmc", "fabric-loader") | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import dan200.computercraft.shared.command.CommandComputerCraft; | ||||
| import dan200.computercraft.shared.config.Config; | ||||
| import dan200.computercraft.shared.config.ConfigSpec; | ||||
| import dan200.computercraft.shared.details.FluidDetails; | ||||
| import dan200.computercraft.shared.integration.CreateIntegration; | ||||
| import dan200.computercraft.shared.network.NetworkMessages; | ||||
| import dan200.computercraft.shared.peripheral.commandblock.CommandBlockPeripheral; | ||||
| import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods; | ||||
| @@ -143,6 +144,8 @@ public class ComputerCraft { | ||||
| 
 | ||||
|     private static <B extends FriendlyByteBuf, T extends CustomPacketPayload> void registerPayloadType(PayloadTypeRegistry<B> registry, CustomPacketPayload.TypeAndCodec<B, T> type) { | ||||
|         registry.register(type.type(), type.codec()); | ||||
| 
 | ||||
|         if (FabricLoader.getInstance().isModLoaded(CreateIntegration.ID)) CreateIntegration.setup(); | ||||
|     } | ||||
| 
 | ||||
|     private record ReloadListener(String name, PreparableReloadListener listener) | ||||
|   | ||||
| @@ -120,6 +120,7 @@ dependencies { | ||||
|     clientCompileOnly(variantOf(libs.emi) { classifier("api") }) | ||||
|     compileOnly(libs.bundles.externalMods.forge.compile) | ||||
|     runtimeOnly(libs.bundles.externalMods.forge.runtime) { cct.exclude(this) } | ||||
|     compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false } | ||||
| 
 | ||||
|     implementation("net.neoforged:neoforge:${libs.versions.neoForge.get()}") | ||||
| 
 | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import dan200.computercraft.shared.ModRegistry; | ||||
| import dan200.computercraft.shared.config.Config; | ||||
| import dan200.computercraft.shared.config.ConfigSpec; | ||||
| import dan200.computercraft.shared.details.FluidData; | ||||
| import dan200.computercraft.shared.integration.CreateIntegration; | ||||
| import dan200.computercraft.shared.network.NetworkMessage; | ||||
| import dan200.computercraft.shared.network.NetworkMessages; | ||||
| import dan200.computercraft.shared.network.client.ClientNetworkContext; | ||||
| @@ -39,6 +40,7 @@ import net.minecraft.server.level.ServerPlayer; | ||||
| import net.minecraft.world.level.block.entity.BlockEntityType; | ||||
| import net.neoforged.bus.api.IEventBus; | ||||
| import net.neoforged.bus.api.SubscribeEvent; | ||||
| import net.neoforged.fml.ModList; | ||||
| import net.neoforged.fml.ModLoadingContext; | ||||
| import net.neoforged.fml.common.EventBusSubscriber; | ||||
| import net.neoforged.fml.common.Mod; | ||||
| @@ -108,6 +110,8 @@ public final class ComputerCraft { | ||||
|         ForgeComputerCraftAPI.registerGenericCapability(Capabilities.EnergyStorage.BLOCK); | ||||
| 
 | ||||
|         ForgeDetailRegistries.FLUID_STACK.addProvider(FluidData::fill); | ||||
| 
 | ||||
|         if (ModList.get().isLoaded(CreateIntegration.ID)) event.enqueueWork(CreateIntegration::setup); | ||||
|     } | ||||
| 
 | ||||
|     @SubscribeEvent | ||||
|   | ||||
| @@ -17,12 +17,11 @@ ensure language files are mostly correct. | ||||
|  | ||||
| import json | ||||
| import pathlib | ||||
| from collections import OrderedDict | ||||
|  | ||||
| root = pathlib.Path("projects/common/src/main/resources/assets/computercraft/lang") | ||||
|  | ||||
| with open("projects/fabric/src/generated/resources/assets/computercraft/lang/en_us.json", encoding="utf-8") as file: | ||||
|     en_us = json.load(file, object_hook=OrderedDict) | ||||
| with open("projects/common/src/generated/resources/assets/computercraft/lang/en_us.json", encoding="utf-8") as file: | ||||
|     en_us = json.load(file) | ||||
|  | ||||
| for path in root.glob("*.json"): | ||||
|     if path.name == "en_us.json": | ||||
| @@ -31,7 +30,7 @@ for path in root.glob("*.json"): | ||||
|     with path.open(encoding="utf-8") as file: | ||||
|         lang = json.load(file) | ||||
|  | ||||
|     out = OrderedDict() | ||||
|     out = {} | ||||
|     missing = 0 | ||||
|     for k in en_us.keys(): | ||||
|         if k not in lang: | ||||
| @@ -46,4 +45,6 @@ for path in root.glob("*.json"): | ||||
|         file.write("\n") | ||||
|  | ||||
|     if missing > 0: | ||||
|         print("{} has {} missing translations.".format(path.name, missing)) | ||||
|         print("{} has {} missing translations. {:.2f}% complete".format(path.name, missing, len(out) / len(en_us) * 100)) | ||||
|     else: | ||||
|         print("{} is complete".format(path.name)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates