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

Compare commits

...

31 Commits

Author SHA1 Message Date
Jonathan Coates
08d4f91c8b Bump CC:T to 1.112.0 2024-07-31 07:05:08 +01:00
Jonathan Coates
b9eac4e509 Computer components (#1915)
This adds a new mechanism for attaching additional objects to a
computer, allowing them to be queried by other mods. This is primarily
designed for mods which add external APIs, allowing them to add APIs
which depend on the computer's position or can interact with the turtle
inventory.

I will stress that the use-cases for custom APIs are few and far
between. Almost all the time a peripheral would be the better option,
and I am wary that this PR will encourage misuse of APIs. However, there
are some legitimate use-cases, and I think we should enable them.

 - Add a new "ComputerComponent" class, and several built-in components
   (for turtle, pocket and command computers).

 - Add a method to `IComputerSystem` to read a component from the
   computer. We also add methods to get the level and position of the
   computer.

 - Move all our existing APIs (built-in turtle, pocket, command) to use
   the public API.
2024-07-31 06:57:38 +01:00
Jonathan Coates
dc3d8ea198 Move API factories to the common package
We don't actually use this functionality in other projects (e.g.
emulators). In fact the method to add new APIs only exists in the mod
itself!

We still need some mechanism to remove mounts when the computer is
shutdown. We add a new ApiLifecycle interface (with startup and
shutdown hooks), and use those in the ComputerSystem impl.
2024-07-29 19:46:25 +01:00
Jonathan Coates
cbe075b001 Expose level+position in IPocketAccess
This allows pocket upgrades (modems and speakers) to read the position
directly, rather than checking whether the entity is present.
2024-07-28 21:15:25 +01:00
Jonathan Coates
ed0b156e05 Attempt at splitting up pocket computer logic
Oh, I hate the pocket computer code so much. Minecraft was really not
designed to attach this sort of behaviour to computers. This commit is
an attempt of cleaning this up[^1].

Firstly, we move the the pocket computer state (upgrades, light) out of
PocketServerComputer and into a new PocketBrain class. This now acts as
the sole source-of-truth, with all state being synced back to the
original item stack on the entity tick.

This also adds a new PocketHolder interface, which generalises over the
various types that can hold a pocket computer (players and item
entities right now, possibly lecterns in the future).

[^1]: I'd say simplifying, but this would be a lie.
2024-07-28 21:13:07 +01:00
Jonathan Coates
4dd0735066 Register modems as attached to their adjacent block
In c8eadf4011 we marked our various modems
as "brittle", which ensures they do not pop-off computers when the whole
structure moves.

However, this still requires the modem to be glued — if the modem is
outside the superglue range, it will still pop off. We can fix it by
registering a special "attached check" for the various modem blocks,
which says that the modem should be moved when the adjacent block does.

Fixes #1913
2024-07-26 18:28:13 +01:00
Jonathan Coates
38e516d7c7 Update to Reuse 4.0 2024-07-26 18:28:00 +01:00
Jonathan Coates
70a31855ac Only send pocket computer updates to players in range
Previously we sent it to all players in the current level. This updates
the check to only send it to players tracking the current chunk.
2024-07-26 10:07:26 +01:00
Jonathan Coates
6c8e64ffcd Add get/setUpgrade to IPocketAccess
We have similar methods in ITurtleAccess, so makes sense for them to be
here too.
2024-07-26 09:51:09 +01:00
Jonathan Coates
7285c32d58 Document that the speaker re-encoded audio samples 2024-07-25 20:32:08 +01:00
Jonathan Coates
99c60ac54b Remove IComputerBlockEntity
We only really made use of it in the has_computer_id loot condition, so
probably easier to remove it.
2024-07-25 09:28:00 +01:00
Jonathan Coates
63e40cf3cb Add a cc.strings.split method
This is largely copied from metis, with the documentation updated.
2024-07-24 22:18:50 +01:00
Jonathan Coates
1d45935a25 Update links to IRC
The EsperNet webchat has been decommissioned, with no plans to
replace. KiwiIRC is the recommended replacement, but I'm not comfortable
using it as a drop-in replacement[^1], so I've rephrased this section to
make it more clear.

[^1]: There seems to be ongoing issues with TLS certificates
  (https://github.com/kiwiirc/kiwiirc/issues/1870), which meant it
  wasn't usable when I first tried.
2024-07-24 21:38:14 +01:00
Jonathan Coates
f80373e7a2 Add bounds check to cc.strings.wrap
Fixes #1905, closes #1906.

Co-authored-by: Lupus590 <lupussolitarius590@gmail.com>
2024-07-24 19:40:10 +01:00
Jonathan Coates
63185629b7 Use "require" in textutils
This avoids us having to support requireless environments inside
cc.strings.

I do kinda wonder if os.loadAPI-loaded files should also have their own
shared "require", just so we're not loading 10 copies of cc.expect.
2024-07-24 19:40:10 +01:00
Jonathan Coates
4bfb9ac323 Fix/update language checker script
- Update location of the generated language file to point to common
   rather than Fabric.
 - Remove usage of OrderedDict, as dicts are ordered on recent versions
   of Python.
2024-07-24 19:40:09 +01:00
Jonathan Coates
5926b6c994 Update Gradle to 8.9
Fix several deprecation warnings, and specify the toolchain required to
launch the daemon.
2024-07-24 19:40:09 +01:00
Jonathan Coates
f5ed43584d Add tests for turtle equipping and crafting 2024-07-24 19:40:09 +01:00
csqrb
d77f5f135f Preserve item data when upgrading pocket computers (#1888) 2024-07-03 07:21:02 +00:00
Jonathan Coates
7744d2663b Fix heights of turtle flags
They were 0.5 squares too high, so the textures were a little stretched.
2024-06-27 20:41:42 +01:00
Jonathan Coates
4566cb8273 Add path-based error constructor to FileSystemException
This doesn't change any functionality, but means we only construct
"/{path}: {message}" strings in one location.
2024-06-26 21:12:44 +01:00
Jonathan Coates
052e7a7ae5 Make FileSystem.toLocal private
Use a custom to-local function in the various ArchiveMounts, which don't
faff around with sanitising paths.
2024-06-26 21:12:05 +01:00
Jonathan Coates
0895200681 Small bits of cleanup
Build system:
 - Switch to our new maven server. This has a cleaner separation between
   published packages and mirrored packages, to avoid leaking those into
   other people's builds.
 - Update Gradle and Loom versions.

Code:
 - Link to definitions instead in the breaking changes page.
 - Fix several unused variable warnings.

Other:
 - Remove unsupported Minecraft versions from the issue template.
2024-06-26 18:07:57 +01:00
Jonathan Coates
09d0f563b7 Add 1.21 to version list 2024-06-23 09:14:32 +01:00
Jonathan Coates
e188f1d3fa Link to os.time in os.setAlarm docs 2024-06-23 08:43:42 +01:00
Jonathan Coates
819a4f7231 Hide a few ServerComputer internals 2024-06-22 18:48:31 +01:00
Jonathan Coates
898cb2a95d Remove CommandComputerBlockEntity
Due to the earlier commits, the only functionality this block entity
adds is to register the command API. This commit:

 - Add the command API when constructing the ServerComputer instead.
   This is not a good long-term solution (I think we need to make API
   factories more powerful), but is sufficient for now.

 - Replace usages of CommandComputerBlockEntity with a normal
   ComputerBlockEntity.
2024-06-22 18:19:00 +01:00
Jonathan Coates
03a8f83191 Unify command computer permission checks
- Move the command permisssion checks to a new
   ComputerFamily.checkUsable method (from
   CommandComputerBlockEntity and ViewComputerMenu). I don't feel great
   about putting new functionality in ComputerFamily (trying to move
   away from it), but I think this is fine for now.

 - Use this method from within the computer menu and computer block, to
   check whether computers can be interacted with.

 - Remove ViewComputerMenu, as it now no longer needs any special
   is-usable logic.
2024-06-22 18:08:04 +01:00
Jonathan Coates
aef92c8ebc Remove pocket computer GUI
Historically we used to have separate menu types for computers and
pocket computers, as the screen had to be initialised with the correct
terminal size.

However, as of c49547b962 (which was
admittedly two years ago now), we have the terminal available when
constructing the screen, and so the code for the two is identical.

This change actually merges the two screens, replacing usages of the
pocket computer UI with the computer one.
2024-06-22 17:41:49 +01:00
Jonathan Coates
571ea794a8 Decouple CommandAPI from the command computer BE
All we really need for its implementation is a level and position, which
we can get directly from the server block entity!
2024-06-22 17:18:21 +01:00
Jonathan Coates
e81af93043 Move JEI to client folder
I hadn't realised this, but plugins are only loaded on the client. This
is useful on 1.21, as we now have easy access to a holder lookup.
2024-06-22 13:23:03 +01:00
104 changed files with 1916 additions and 920 deletions

View File

@@ -8,10 +8,8 @@ body:
label: Minecraft Version
description: What version of Minecraft are you using?
options:
- 1.16.x
- 1.18.x
- 1.19.x
- 1.20.x
- 1.20.1
- 1.21.x
validations:
required: true
- type: input

View File

@@ -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

View File

@@ -1,99 +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_upgrades/*
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/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

View File

@@ -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 17 (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

View File

@@ -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").
@@ -39,7 +40,7 @@ on is present.
```groovy
repositories {
maven {
url "https://squiddev.cc/maven/"
url "https://maven.squiddev.cc"
content {
includeGroup("cc.tweaked")
}
@@ -86,6 +87,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"

110
REUSE.toml Normal file
View File

@@ -0,0 +1,110 @@
# 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_upgrades/**",
"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/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"

View File

@@ -36,7 +36,7 @@ repositories {
}
}
maven("https://squiddev.cc/maven") {
maven("https://maven.squiddev.cc") {
name = "SquidDev"
content {
includeGroup("cc.tweaked.vanilla-extract")

View File

@@ -38,7 +38,7 @@ java {
repositories {
mavenCentral()
val mainMaven = maven("https://squiddev.cc/maven") {
val mainMaven = maven("https://maven.squiddev.cc/mirror") {
name = "SquidDev"
}
@@ -54,6 +54,7 @@ repositories {
filter {
includeGroup("cc.tweaked")
// Things we mirror
includeGroup("com.simibubi.create")
includeGroup("commoble.morered")
includeGroup("dev.architectury")
includeGroup("dev.emi")
@@ -133,8 +134,8 @@ tasks.processResources {
tasks.withType(AbstractArchiveTask::class.java).configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
dirMode = Integer.valueOf("755", 8)
fileMode = Integer.valueOf("664", 8)
filePermissions {}
dirPermissions {}
}
tasks.jar {

View File

@@ -38,7 +38,7 @@ publishing {
}
repositories {
maven("https://squiddev.cc/maven") {
maven("https://maven.squiddev.cc") {
name = "SquidDev"
credentials(PasswordCredentials::class)

View File

@@ -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 {

View File

@@ -155,3 +155,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)
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -25,13 +25,13 @@ as documentation for breaking changes and "gotchas" one should look out for betw
- Update to Lua 5.2:
- Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs.
- Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. `getfenv`/`setfenv`
now only work on Lua functions with an `_ENV` upvalue. `getfenv` will return the global environment when called
with other functions, and `setfenv` will have no effect.
- `load`/`loadstring` defaults to using the global environment (`_G`) rather than the current coroutine's
- Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. [`getfenv`]/[`setfenv`]
now only work on Lua functions with an `_ENV` upvalue. [`getfenv`] will return the global environment when called
with other functions, and [`setfenv`] will have no effect.
- [`load`]/[`loadstring`] defaults to using the global environment (`_G`) rather than the current coroutine's
environment.
- Support for dumping functions (`string.dump`) and loading binary chunks has been removed.
- `math.random` now uses Lua 5.4's random number generator.
- Support for dumping functions ([`string.dump`]) and loading binary chunks has been removed.
- [`math.random`] now uses Lua 5.4's random number generator.
- File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
@@ -44,7 +44,7 @@ as documentation for breaking changes and "gotchas" one should look out for betw
`keys.enter` constant was queued when the key was pressed)
- Minecraft 1.13 removed the concept of item damage and block metadata (see ["The Flattening"][flattening]). As a
result `turtle.inspect` no longer provides block metadata, and `turtle.getItemDetail` no longer provides damage.
result [`turtle.inspect`] no longer provides block metadata, and [`turtle.getItemDetail`] no longer provides damage.
- Block states (`turtle.inspect().state`) should provide all the same information as block metadata, but in a much
more understandable format.
@@ -70,7 +70,7 @@ as documentation for breaking changes and "gotchas" one should look out for betw
- Unlabelled computers and turtles now keep their ID when broken, meaning that unlabelled computers/items do not stack.
## ComputerCraft 1.80pr1 {#cc-1.80}
- Programs run via `shell.run` are now started in their own isolated environment. This means globals set by programs
- Programs run via [`shell.run`] are now started in their own isolated environment. This means globals set by programs
will not be accessible outside of this program.
- Programs containing `/` are looked up in the current directory and are no longer looked up on the path. For instance,

View File

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

View File

@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=17

View File

@@ -46,6 +46,8 @@ oculus = "1.2.5"
rei = "12.0.626"
rubidium = "0.6.1"
sodium = "mc1.20-0.4.10"
create-forge = "0.5.1.f-33"
create-fabric = "0.5.1-f-build.1467+mc1.20.1"
# Testing
hamcrest = "2.2"
@@ -59,7 +61,7 @@ checkstyle = "10.14.1"
curseForgeGradle = "1.0.14"
errorProne-core = "2.27.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.6.7"
fabric-loom = "1.7.1"
forgeGradle = "6.0.21"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
@@ -71,7 +73,7 @@ minotaur = "2.+"
nullAway = "0.10.25"
spotless = "6.23.3"
taskTree = "2.1.1"
teavm = "0.10.0-SQUID.4"
teavm = "0.11.0-SQUID.1"
vanillaExtract = "0.1.3"
versionCatalogUpdate = "0.8.1"
@@ -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.20.1-common-api", version.ref = "jei" }
jei-fabric = { module = "mezz.jei:jei-1.20.1-fabric", version.ref = "jei" }

View File

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

2
gradlew vendored
View File

@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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 + ")";
}
}

View File

@@ -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");
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -6,10 +6,14 @@ package dan200.computercraft.api.pocket;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
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;
import java.util.Map;
@@ -17,7 +21,22 @@ import java.util.Map;
/**
* 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>
@@ -64,6 +83,26 @@ public interface IPocketAccess {
*/
void setLight(int colour);
/**
* Get the currently equipped upgrade.
*
* @return The currently equipped upgrade.
* @see #getUpgradeNBTData()
* @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>
@@ -73,6 +112,7 @@ public interface IPocketAccess {
* @see #updateUpgradeNBTData()
* @see UpgradeBase#getUpgradeItem(CompoundTag)
* @see UpgradeBase#getUpgradeData(ItemStack)
* @see #getUpgrade()
*/
CompoundTag getUpgradeNBTData();

View File

@@ -43,6 +43,7 @@ dependencies {
clientImplementation(clientClasses(project(":common-api")))
compileOnly(libs.bundles.externalMods.common)
compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false }
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
annotationProcessorEverywhere(libs.autoService)

View File

@@ -25,7 +25,6 @@ import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem;
import net.minecraft.Util;
@@ -83,7 +82,6 @@ public final class ClientRegistry {
*/
public static void registerMainThread(RegisterItemProperty itemProperties) {
MenuScreens.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
MenuScreens.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER.get(), ComputerScreen::new);
MenuScreens.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
MenuScreens.register(ModRegistry.Menus.TURTLE.get(), TurtleScreen::new);
@@ -91,8 +89,6 @@ public final class ClientRegistry {
MenuScreens.register(ModRegistry.Menus.DISK_DRIVE.get(), DiskDriveScreen::new);
MenuScreens.register(ModRegistry.Menus.PRINTOUT.get(), PrintoutScreen::new);
MenuScreens.<ViewComputerMenu, ComputerScreen<ViewComputerMenu>>register(ModRegistry.Menus.VIEW_COMPUTER.get(), ComputerScreen::new);
registerItemProperty(itemProperties, "state",
new UnclampedPropertyFunction((stack, world, player, random) -> {
var computer = ClientPocketComputers.get(stack);

View File

@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.jei;
package dan200.computercraft.client.integration.jei;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;

View File

@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.jei;
package dan200.computercraft.client.integration.jei;
import dan200.computercraft.shared.integration.UpgradeRecipeGenerator;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;

View File

@@ -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 {

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.shared;
import com.mojang.brigadier.arguments.ArgumentType;
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;
@@ -23,13 +24,12 @@ 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.CommandComputerBlockEntity;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.computer.items.CommandComputerItem;
import dan200.computercraft.shared.computer.items.ComputerItem;
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
@@ -66,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;
@@ -75,14 +76,17 @@ import dan200.computercraft.shared.recipe.CustomShapelessRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
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.TurtleOverlayRecipe;
import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
import dan200.computercraft.shared.turtle.upgrades.*;
import dan200.computercraft.shared.util.ComponentMap;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
@@ -104,6 +108,7 @@ import net.minecraft.world.level.block.state.BlockState;
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;
@@ -142,8 +147,7 @@ public final class ModRegistry {
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), BlockEntities.COMPUTER_NORMAL));
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), BlockEntities.COMPUTER_ADVANCED));
public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command",
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command",
() -> new CommandComputerBlock<>(computerProperties().strength(-1, 6000000.0F), BlockEntities.COMPUTER_COMMAND));
public static final RegistryEntry<TurtleBlock> TURTLE_NORMAL = REGISTRY.register("turtle_normal",
@@ -186,8 +190,8 @@ public final class ModRegistry {
ofBlock(Blocks.COMPUTER_NORMAL, (p, s) -> new ComputerBlockEntity(BlockEntities.COMPUTER_NORMAL.get(), p, s, ComputerFamily.NORMAL));
public static final RegistryEntry<BlockEntityType<ComputerBlockEntity>> COMPUTER_ADVANCED =
ofBlock(Blocks.COMPUTER_ADVANCED, (p, s) -> new ComputerBlockEntity(BlockEntities.COMPUTER_ADVANCED.get(), p, s, ComputerFamily.ADVANCED));
public static final RegistryEntry<BlockEntityType<CommandComputerBlockEntity>> COMPUTER_COMMAND =
ofBlock(Blocks.COMPUTER_COMMAND, (p, s) -> new CommandComputerBlockEntity(BlockEntities.COMPUTER_COMMAND.get(), p, s));
public static final RegistryEntry<BlockEntityType<ComputerBlockEntity>> COMPUTER_COMMAND =
ofBlock(Blocks.COMPUTER_COMMAND, (p, s) -> new ComputerBlockEntity(BlockEntities.COMPUTER_COMMAND.get(), p, s, ComputerFamily.COMMAND));
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_NORMAL =
ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, () -> Config.turtleFuelLimit, ComputerFamily.NORMAL));
@@ -293,9 +297,6 @@ public final class ModRegistry {
public static final RegistryEntry<MenuType<ComputerMenuWithoutInventory>> COMPUTER = REGISTRY.register("computer",
() -> ContainerData.toType(ComputerContainerData::new, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.COMPUTER.get(), id, inv, data)));
public static final RegistryEntry<MenuType<ComputerMenuWithoutInventory>> POCKET_COMPUTER = REGISTRY.register("pocket_computer",
() -> ContainerData.toType(ComputerContainerData::new, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.POCKET_COMPUTER.get(), id, inv, data)));
public static final RegistryEntry<MenuType<ComputerMenuWithoutInventory>> POCKET_COMPUTER_NO_TERM = REGISTRY.register("pocket_computer_no_term",
() -> ContainerData.toType(ComputerContainerData::new, (id, inv, data) -> new ComputerMenuWithoutInventory(Menus.POCKET_COMPUTER_NO_TERM.get(), id, inv, data)));
@@ -313,9 +314,6 @@ public final class ModRegistry {
HeldItemContainerData::new,
(id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.getHand())
));
public static final RegistryEntry<MenuType<ViewComputerMenu>> VIEW_COMPUTER = REGISTRY.register("view_computer",
() -> ContainerData.toType(ComputerContainerData::new, ViewComputerMenu::new));
}
static class ArgumentTypes {
@@ -456,6 +454,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);
}

View File

@@ -18,7 +18,7 @@ import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.metrics.basic.Aggregate;
import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
@@ -269,7 +269,7 @@ public final class CommandComputerCraft {
@Override
public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
return new ViewComputerMenu(id, player, computer);
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer);
}
});
return 1;

View File

@@ -6,19 +6,23 @@ 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.blocks.CommandComputerBlockEntity;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,10 +35,13 @@ import java.util.*;
public class CommandAPI implements ILuaAPI {
private static final Logger LOG = LoggerFactory.getLogger(CommandAPI.class);
private final CommandComputerBlockEntity computer;
private final IComputerSystem computer;
private final AdminComputer admin;
private final OutputReceiver receiver = new OutputReceiver();
public CommandAPI(CommandComputerBlockEntity computer) {
public CommandAPI(IComputerSystem computer, AdminComputer admin) {
this.computer = computer;
this.admin = admin;
}
@Override
@@ -48,15 +55,14 @@ public class CommandAPI implements ILuaAPI {
private Object[] doCommand(String command) {
var server = computer.getLevel().getServer();
if (server == null || !server.isCommandBlockEnabled()) {
if (!server.isCommandBlockEnabled()) {
return new Object[]{ false, createOutput("Command blocks disabled by server") };
}
var commandManager = server.getCommands();
var receiver = computer.getReceiver();
try {
receiver.clearOutput();
var result = commandManager.performPrefixedCommand(computer.getSource(), command);
var result = commandManager.performPrefixedCommand(getSource(), command);
return new Object[]{ result > 0, receiver.copyOutput(), result };
} catch (Throwable t) {
LOG.error(Logging.JAVA_ERROR, "Error running command.", t);
@@ -134,7 +140,6 @@ public class CommandAPI implements ILuaAPI {
public final List<String> list(IArguments args) throws LuaException {
var server = computer.getLevel().getServer();
if (server == null) return List.of();
CommandNode<CommandSourceStack> node = server.getCommands().getDispatcher().getRoot();
for (var j = 0; j < args.count(); j++) {
var name = args.getString(j);
@@ -161,7 +166,7 @@ public class CommandAPI implements ILuaAPI {
@LuaFunction
public final Object[] getBlockPosition() {
// This is probably safe to do on the Lua thread. Probably.
var pos = computer.getBlockPos();
var pos = computer.getPosition();
return new Object[]{ pos.getX(), pos.getY(), pos.getZ() };
}
@@ -186,7 +191,6 @@ public class CommandAPI implements ILuaAPI {
* @throws LuaException If trying to get information about more than 4096 blocks.
* @cc.since 1.76
* @cc.changed 1.99 Added {@code dimension} argument.
*
* @cc.usage Print out all blocks in a cube around the computer.
*
* <pre>{@code
@@ -220,7 +224,7 @@ public class CommandAPI implements ILuaAPI {
Math.max(minY, maxY),
Math.max(minZ, maxZ)
);
if (world == null || !world.isInWorldBounds(min) || !world.isInWorldBounds(max)) {
if (!world.isInWorldBounds(min) || !world.isInWorldBounds(max)) {
throw new LuaException("Co-ordinates out of range");
}
@@ -265,10 +269,9 @@ public class CommandAPI implements ILuaAPI {
}
private Level getLevel(Optional<String> id) throws LuaException {
var currentLevel = (ServerLevel) computer.getLevel();
if (currentLevel == null) throw new LuaException("No world exists");
var currentLevel = computer.getLevel();
if (!id.isPresent()) return currentLevel;
if (id.isEmpty()) return currentLevel;
var dimensionId = ResourceLocation.tryParse(id.get());
if (dimensionId == null) throw new LuaException("Invalid dimension name");
@@ -278,4 +281,52 @@ public class CommandAPI implements ILuaAPI {
return level;
}
private CommandSourceStack getSource() {
var name = "@";
var label = computer.getLabel();
if (label != null) name = label;
return new CommandSourceStack(receiver,
Vec3.atCenterOf(computer.getPosition()), Vec2.ZERO,
computer.getLevel(), admin.permissionLevel(),
name, Component.literal(name),
computer.getLevel().getServer(), null
);
}
/**
* A {@link CommandSource} that consumes output messages and stores them to a list.
*/
private final class OutputReceiver implements CommandSource {
private final List<String> output = new ArrayList<>();
void clearOutput() {
output.clear();
}
List<String> copyOutput() {
return List.copyOf(output);
}
@Override
public void sendSystemMessage(Component textComponent) {
output.add(textComponent.getString());
}
@Override
public boolean acceptsSuccess() {
return true;
}
@Override
public boolean acceptsFailure() {
return true;
}
@Override
public boolean shouldInformAdmins() {
return computer.getLevel().getGameRules().getBoolean(GameRules.RULE_COMMANDBLOCKOUTPUT);
}
}
}

View File

@@ -146,7 +146,7 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
super.setPlacedBy(world, pos, state, placer, stack);
var tile = world.getBlockEntity(pos);
if (!world.isClientSide && tile instanceof IComputerBlockEntity computer && stack.getItem() instanceof IComputerItem item) {
if (!world.isClientSide && tile instanceof AbstractComputerBlockEntity computer && stack.getItem() instanceof IComputerItem item) {
var id = item.getComputerID(stack);
if (id != -1) computer.setComputerID(id);

View File

@@ -38,7 +38,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";
@@ -81,7 +81,8 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
}
public boolean isUsable(Player player) {
return BaseContainerBlockEntity.canUnlock(player, lockCode, getDisplayName())
return getFamily().checkUsable(player)
&& BaseContainerBlockEntity.canUnlock(player, lockCode, getDisplayName())
&& Container.stillValidBlockEntity(this, player, getInteractRange());
}
@@ -299,17 +300,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;
@@ -317,7 +315,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;
@@ -327,7 +324,6 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
BlockEntityHelpers.updateBlock(this);
}
@Override
public ComputerFamily getFamily() {
return family;
}

View File

@@ -15,7 +15,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
* @param <T> The type of the computer block entity.
* @see dan200.computercraft.shared.computer.items.CommandComputerItem
*/
public class CommandComputerBlock<T extends CommandComputerBlockEntity> extends ComputerBlock<T> implements GameMasterBlock {
public class CommandComputerBlock<T extends ComputerBlockEntity> extends ComputerBlock<T> implements GameMasterBlock {
public CommandComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings, type);
}

View File

@@ -1,114 +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.apis.CommandAPI;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import net.minecraft.commands.CommandSource;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.List;
public class CommandComputerBlockEntity extends ComputerBlockEntity {
public class CommandReceiver implements CommandSource {
private final List<String> output = new ArrayList<>();
public void clearOutput() {
output.clear();
}
public List<String> copyOutput() {
return new ArrayList<>(output);
}
@Override
public void sendSystemMessage(Component textComponent) {
output.add(textComponent.getString());
}
@Override
public boolean acceptsSuccess() {
return true;
}
@Override
public boolean acceptsFailure() {
return true;
}
@Override
public boolean shouldInformAdmins() {
return getLevel().getGameRules().getBoolean(GameRules.RULE_COMMANDBLOCKOUTPUT);
}
}
private final CommandReceiver receiver;
public CommandComputerBlockEntity(BlockEntityType<? extends ComputerBlockEntity> type, BlockPos pos, BlockState state) {
super(type, pos, state, ComputerFamily.COMMAND);
receiver = new CommandReceiver();
}
public CommandReceiver getReceiver() {
return receiver;
}
public CommandSourceStack getSource() {
var computer = getServerComputer();
var name = "@";
if (computer != null) {
var label = computer.getLabel();
if (label != null) name = label;
}
return new CommandSourceStack(receiver,
Vec3.atCenterOf(worldPosition), Vec2.ZERO,
(ServerLevel) getLevel(), 2,
name, Component.literal(name),
getLevel().getServer(), null
);
}
@Override
protected ServerComputer createComputer(int id) {
var computer = super.createComputer(id);
computer.addAPI(new CommandAPI(this));
return computer;
}
@Override
public boolean isUsable(Player player) {
return isCommandUsable(player) && super.isUsable(player);
}
public static boolean isCommandUsable(Player player) {
var server = player.getServer();
if (server == null || !server.isCommandBlockEnabled()) {
player.displayClientMessage(Component.translatable("advMode.notEnabled"), true);
return false;
} else if (!canUseCommandBlock(player)) {
player.displayClientMessage(Component.translatable("advMode.notAllowed"), true);
return false;
}
return true;
}
private static boolean canUseCommandBlock(Player player) {
return Config.commandRequireCreative ? player.canUseGameMasterBlocks() : player.hasPermissions(2);
}
}

View File

@@ -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()
);
}
@@ -67,7 +69,7 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Nullable
@Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, inventory, this::isUsableByPlayer, createServerComputer(), getFamily());
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, inventory, this::isUsableByPlayer, createServerComputer());
}
public IPeripheral peripheral() {

View File

@@ -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();
}

View File

@@ -4,8 +4,45 @@
package dan200.computercraft.shared.computer.core;
import dan200.computercraft.shared.config.Config;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
public enum ComputerFamily {
NORMAL,
ADVANCED,
COMMAND,
COMMAND;
/**
* Check whether computers with this family can be used by the provided player.
* <p>
* This method is not pure. On failure, the method may send a message to the player telling them why they cannot
* interact with the computer.
*
* @param player The player trying to use a computer.
* @return Whether this computer family can be used.
*/
public boolean checkUsable(Player player) {
return switch (this) {
case NORMAL, ADVANCED -> true;
case COMMAND -> checkCommandUsable(player);
};
}
private static boolean checkCommandUsable(Player player) {
var server = player.getServer();
if (server == null || !server.isCommandBlockEnabled()) {
player.displayClientMessage(Component.translatable("advMode.notEnabled"), true);
return false;
} else if (!canUseCommandBlock(player)) {
player.displayClientMessage(Component.translatable("advMode.notAllowed"), true);
return false;
}
return true;
}
private static boolean canUseCommandBlock(Player player) {
return Config.commandRequireCreative ? player.canUseGameMasterBlocks() : player.hasPermissions(2);
}
}

View File

@@ -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);
}
}

View File

@@ -7,7 +7,6 @@ package dan200.computercraft.shared.computer.core;
import com.google.common.annotations.VisibleForTesting;
import dan200.computercraft.api.filesystem.FileOperationException;
import dan200.computercraft.core.filesystem.ArchiveMount;
import dan200.computercraft.core.filesystem.FileSystem;
import net.minecraft.ResourceLocationException;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
@@ -65,9 +64,10 @@ public final class ResourceMount extends ArchiveMount<ResourceMount.FileEntry> {
existingNamespace = file.getNamespace();
if (!file.getNamespace().equals(namespace)) continue;
if (!FileSystem.contains(subPath, file.getPath())) continue; // Some packs seem to include the parent?
var localPath = FileSystem.toLocal(file.getPath(), subPath);
var localPath = getLocalPath(file.getPath(), subPath);
if (localPath == null) continue;
try {
getOrCreateChild(newRoot, localPath, this::createEntry);
} catch (ResourceLocationException e) {

View File

@@ -5,14 +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.core.apis.IAPIEnvironment;
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.impl.ApiFactories;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
@@ -21,8 +23,10 @@ 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;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
@@ -47,7 +51,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,8 +63,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);
// 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() {
@@ -70,32 +94,20 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return level;
}
public void setLevel(ServerLevel level) {
this.level = level;
}
public BlockPos getPosition() {
return position;
}
public void setPosition(BlockPos pos) {
position = new BlockPos(pos);
}
public IAPIEnvironment getAPIEnvironment() {
return computer.getAPIEnvironment();
}
public Computer getComputer() {
return computer;
public void setPosition(ServerLevel level, BlockPos pos) {
this.level = level;
position = pos.immutable();
}
protected void markTerminalChanged() {
terminalChanged.set(true);
}
public void tickServer() {
protected void tickServer() {
ticksSincePing++;
computer.tick();
if (terminalChanged.getAndSet(false)) onTerminalChanged();
@@ -113,10 +125,15 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
ticksSincePing = 0;
}
public boolean hasTimedOut() {
boolean hasTimedOut() {
return ticksSincePing > 100;
}
/**
* Get a bitmask returning which sides on the computer have changed, resetting the internal state.
*
* @return What sides on the computer have changed.
*/
public int pollAndResetChanges() {
return computer.pollAndResetChanges();
}
@@ -135,6 +152,17 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
ServerContext.get(level.getServer()).registry().remove(this);
}
/**
* Check whether this computer is usable by a player.
*
* @param player The player trying to use this computer.
* @return Whether this computer can be used.
*/
public final boolean checkUsable(Player player) {
return ServerContext.get(level.getServer()).registry().get(instanceUUID) == this
&& getFamily().checkUsable(player);
}
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
var server = level.getServer();
@@ -175,25 +203,21 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
@Override
public void turnOn() {
// Turn on
computer.turnOn();
}
@Override
public void shutdown() {
// Shutdown
computer.shutdown();
}
@Override
public void reboot() {
// Reboot
computer.reboot();
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
// Queue event
computer.queueEvent(event, arguments);
}
@@ -213,10 +237,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);
}
@@ -245,6 +265,10 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return metrics;
}
public WorkMonitor getMainThreadMonitor() {
return computer.getMainThreadMonitor();
}
@Override
public @Nullable WritableMount createRootMount() {
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit);

View File

@@ -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"));

View File

@@ -59,7 +59,7 @@ public abstract class AbstractComputerMenu extends AbstractContainerMenu impleme
@Override
public boolean stillValid(Player player) {
return canUse.test(player);
return (computer == null || computer.checkUsable(player)) && canUse.test(player);
}
public ComputerFamily getFamily() {

View File

@@ -4,7 +4,6 @@
package dan200.computercraft.shared.computer.inventory;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.container.InvisibleSlot;
import dan200.computercraft.shared.network.container.ComputerContainerData;
@@ -23,9 +22,9 @@ import java.util.function.Predicate;
public class ComputerMenuWithoutInventory extends AbstractComputerMenu {
public ComputerMenuWithoutInventory(
MenuType<? extends AbstractComputerMenu> type, int id, Inventory player, Predicate<Player> canUse,
ServerComputer computer, ComputerFamily family
ServerComputer computer
) {
super(type, id, canUse, family, computer, null);
super(type, id, canUse, computer.getFamily(), computer, null);
addSlots(player);
}

View File

@@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.computer.inventory;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.CommandComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
public class ViewComputerMenu extends ComputerMenuWithoutInventory {
public ViewComputerMenu(int id, Inventory player, ServerComputer computer) {
super(ModRegistry.Menus.VIEW_COMPUTER.get(), id, player, p -> canInteractWith(computer, p), computer, computer.getFamily());
}
public ViewComputerMenu(int id, Inventory player, ComputerContainerData data) {
super(ModRegistry.Menus.VIEW_COMPUTER.get(), id, player, data);
}
private static boolean canInteractWith(ServerComputer computer, Player player) {
// If this computer no longer exists then discard it.
if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceUUID()) != computer) {
return false;
}
// If we're a command computer then ensure we're in creative
if (computer.getFamily() == ComputerFamily.COMMAND && !CommandComputerBlockEntity.isCommandUsable(player)) {
return false;
}
return true;
}
}

View File

@@ -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

View File

@@ -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;
}
});
}
}

View File

@@ -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, new ResourceLocation(mod, name));

View File

@@ -27,7 +27,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
clientId = computer.getInstanceUUID();
state = computer.getState();
lightState = computer.getLight();
lightState = computer.getBrain().getLight();
terminal = sendTerminal ? computer.getTerminalState() : null;
}

View File

@@ -273,16 +273,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.

View File

@@ -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());

View File

@@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.pocket.IPocketAccess;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
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 javax.annotation.Nullable;
import java.util.Collections;
import java.util.Map;
/**
* 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 = UpgradeData.copyOf(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;
IColouredItem.setColourBasic(stack, colour);
PocketComputerItem.setUpgrade(stack, UpgradeData.copyOf(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 CompoundTag getUpgradeNBTData() {
var upgrade = this.upgrade;
return upgrade == null ? new CompoundTag() : upgrade.data();
}
@Override
public void updateUpgradeNBTData() {
dirty = true;
}
@Override
public void invalidatePeripheral() {
var peripheral = upgrade == null ? null : upgrade.upgrade().createPeripheral(this);
computer.setPeripheral(ComputerSide.BACK, peripheral);
}
@Override
@Deprecated(forRemoval = true)
public Map<ResourceLocation, IPeripheral> getUpgrades() {
var upgrade = this.upgrade;
return upgrade == null ? Map.of() : Collections.singletonMap(upgrade.upgrade().getUpgradeID(), computer.getPeripheral(ComputerSide.BACK));
}
@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();
}
}

View File

@@ -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());
}
}
}

View File

@@ -4,12 +4,7 @@
package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.pocket.IPocketAccess;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -18,172 +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.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
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.level.ChunkPos;
import javax.annotation.Nullable;
import java.util.*;
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;
}
public PocketBrain getBrain() {
return brain;
}
@Override
public int getColour() {
return IColouredItem.getColourBasic(stack);
}
@Override
public void setColour(int colour) {
IColouredItem.setColourBasic(stack, colour);
updateUpgradeNBTData();
}
@Override
public int getLight() {
return lightColour;
}
@Override
public void setLight(int colour) {
if (colour < 0 || colour > 0xFFFFFF) colour = -1;
lightColour = colour;
}
@Override
public CompoundTag getUpgradeNBTData() {
return PocketComputerItem.getUpgradeInfo(stack);
}
@Override
public void updateUpgradeNBTData() {
if (entity instanceof Player player) player.getInventory().setChanged();
}
@Override
public void invalidatePeripheral() {
var peripheral = upgrade == null ? null : upgrade.createPeripheral(this);
setPeripheral(ComputerSide.BACK, peripheral);
}
@Override
@Deprecated(forRemoval = true)
public Map<ResourceLocation, IPeripheral> getUpgrades() {
return upgrade == null ? Map.of() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK));
}
public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() {
return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData());
}
/**
* 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) {
PocketComputerItem.setUpgrade(stack, upgrade);
updateUpgradeNBTData();
this.upgrade = upgrade == null ? null : upgrade.upgrade();
invalidatePeripheral();
}
}
public synchronized void updateValues(@Nullable Entity entity, ItemStack stack, @Nullable IPocketUpgrade upgrade) {
if (entity != null) {
setLevel((ServerLevel) entity.getCommandSenderWorld());
setPosition(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();
}
}
@Override
public void tickServer() {
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());
}
}

View File

@@ -45,12 +45,12 @@ public class PocketComputerMenuProvider implements MenuProvider {
@Override
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player entity) {
return new ComputerMenuWithoutInventory(
isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.POCKET_COMPUTER.get(), id, inventory,
isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(), id, inventory,
p -> {
var stack = p.getItemInHand(hand);
return stack.getItem() == item && PocketComputerItem.getServerComputer(assertNonNull(entity.level().getServer()), stack) == computer;
},
computer, item.getFamily()
computer
);
}
}

View File

@@ -14,21 +14,25 @@ import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.shared.common.IColouredItem;
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.IComputerItem;
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.IDAssigner;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
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;
@@ -72,13 +76,33 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return result;
}
private boolean tick(ItemStack stack, Level world, 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.setLevel((ServerLevel) world);
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 ID
var id = computer.getID();
@@ -100,21 +124,20 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
stack.getOrCreateTag().putBoolean(NBT_ON, 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, world, 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
@@ -122,8 +145,11 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
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.level(), entity, computer)) entity.setItem(stack.copy());
if (computer != null) tick(stack, new PocketHolder.ItemEntityHolder(entity), computer.getBrain());
return false;
}
@@ -131,14 +157,18 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
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) {
@@ -188,40 +218,49 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return ComputerCraftAPI.MOD_ID;
}
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) registry.get(getSessionID(stack), getInstanceID(stack));
if (computer == null) {
var computerID = getComputerID(stack);
if (computerID < 0) {
computerID = ComputerCraftAPI.createUniqueNumberedSaveDir(level.getServer(), IDAssigner.COMPUTER);
setComputerID(stack, computerID);
}
computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
var tag = stack.getOrCreateTag();
tag.putInt(NBT_SESSION, registry.getSessionID());
tag.putUUID(NBT_INSTANCE, computer.register());
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();
}
computer.setLevel(level);
return computer;
var computerID = getComputerID(stack);
if (computerID < 0) {
computerID = ComputerCraftAPI.createUniqueNumberedSaveDir(level.getServer(), IDAssigner.COMPUTER);
setComputerID(stack, computerID);
}
var brain = new PocketBrain(holder, getComputerID(stack), getLabel(stack), getFamily(), getUpgradeWithData(stack));
var computer = brain.computer();
var tag = stack.getOrCreateTag();
tag.putInt(NBT_SESSION, registry.getSessionID());
tag.putUUID(NBT_INSTANCE, 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) registry.get(getSessionID(stack), getInstanceID(stack));
}
@Nullable
public static PocketServerComputer getServerComputer(MinecraftServer server, ItemStack stack) {
return (PocketServerComputer) ServerContext.get(server).registry().get(getSessionID(stack), getInstanceID(stack));
return getServerComputer(ServerContext.get(server).registry(), stack);
}
// IComputerItem implementation

View File

@@ -31,8 +31,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);
}

View File

@@ -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.getCommandSenderWorld();
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

View File

@@ -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);

View File

@@ -59,7 +59,6 @@ public final class PocketComputerUpgradeRecipe extends CustomRecipe {
if (computer.isEmpty()) return ItemStack.EMPTY;
var itemComputer = (PocketComputerItem) computer.getItem();
if (PocketComputerItem.getUpgrade(computer) != null) return ItemStack.EMPTY;
// Check for upgrades around the item
@@ -81,10 +80,9 @@ public final class PocketComputerUpgradeRecipe extends CustomRecipe {
if (upgrade == null) return ItemStack.EMPTY;
// Construct the new stack
var computerID = itemComputer.getComputerID(computer);
var label = itemComputer.getLabel(computer);
var colour = itemComputer.getColour(computer);
return itemComputer.create(computerID, label, colour, upgrade);
var newStack = computer.copyWithCount(1);
PocketComputerItem.setUpgrade(newStack, upgrade);
return newStack;
}
@Override

View File

@@ -9,8 +9,8 @@ import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.turtle.TurtleCommand;
import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods;
import dan200.computercraft.shared.turtle.core.*;
@@ -64,11 +64,11 @@ import java.util.Optional;
* @cc.since 1.3
*/
public class TurtleAPI implements ILuaAPI {
private final IAPIEnvironment environment;
private final MetricsObserver metrics;
private final TurtleAccessInternal turtle;
public TurtleAPI(IAPIEnvironment environment, TurtleAccessInternal turtle) {
this.environment = environment;
public TurtleAPI(MetricsObserver metrics, TurtleAccessInternal turtle) {
this.metrics = metrics;
this.turtle = turtle;
}
@@ -78,7 +78,7 @@ public class TurtleAPI implements ILuaAPI {
}
private MethodResult trackCommand(TurtleCommand command) {
environment.observe(Metrics.TURTLE_OPS);
metrics.observe(Metrics.TURTLE_OPS);
return turtle.executeCommand(command);
}
@@ -169,7 +169,7 @@ public class TurtleAPI implements ILuaAPI {
*/
@LuaFunction
public final MethodResult dig(Optional<TurtleSide> side) {
environment.observe(Metrics.TURTLE_OPS);
metrics.observe(Metrics.TURTLE_OPS);
return trackCommand(TurtleToolCommand.dig(InteractDirection.FORWARD, side.orElse(null)));
}
@@ -184,7 +184,7 @@ public class TurtleAPI implements ILuaAPI {
*/
@LuaFunction
public final MethodResult digUp(Optional<TurtleSide> side) {
environment.observe(Metrics.TURTLE_OPS);
metrics.observe(Metrics.TURTLE_OPS);
return trackCommand(TurtleToolCommand.dig(InteractDirection.UP, side.orElse(null)));
}
@@ -199,7 +199,7 @@ public class TurtleAPI implements ILuaAPI {
*/
@LuaFunction
public final MethodResult digDown(Optional<TurtleSide> side) {
environment.observe(Metrics.TURTLE_OPS);
metrics.observe(Metrics.TURTLE_OPS);
return trackCommand(TurtleToolCommand.dig(InteractDirection.DOWN, side.orElse(null)));
}

View File

@@ -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;
@@ -17,9 +18,9 @@ import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.container.BasicContainer;
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.NonNullList;
@@ -75,10 +76,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.getAPIEnvironment(), brain));
brain.setupComputer(computer);
return computer;
}

View File

@@ -290,8 +290,7 @@ public class TurtleBrain implements TurtleAccessInternal {
newTurtle.transferStateFrom(oldOwner);
var computer = newTurtle.createServerComputer();
computer.setLevel((ServerLevel) world);
computer.setPosition(pos);
computer.setPosition((ServerLevel) world, pos);
// Remove the old turtle
oldWorld.removeBlock(oldPos, false);
@@ -608,7 +607,7 @@ public class TurtleBrain implements TurtleAccessInternal {
// If we've got a computer, ensure that we're allowed to perform work.
var computer = owner.getServerComputer();
if (computer != null && !computer.getComputer().getMainThreadMonitor().canWork()) return;
if (computer != null && !computer.getMainThreadMonitor().canWork()) return;
// Pull a new command
var nextCommand = commandQueue.poll();
@@ -621,7 +620,7 @@ public class TurtleBrain implements TurtleAccessInternal {
// Dispatch the callback
if (computer == null) return;
computer.getComputer().getMainThreadMonitor().trackWork(end - start, TimeUnit.NANOSECONDS);
computer.getMainThreadMonitor().trackWork(end - start, TimeUnit.NANOSECONDS);
var callbackID = nextCommand.callbackID();
if (callbackID < 0) return;

View File

@@ -52,7 +52,7 @@ public final class TurtlePlayer {
var player = brain.cachedPlayer;
if (player == null || player.player.getGameProfile() != getProfile(access.getOwningPlayer())
|| player.player.getCommandSenderWorld() != access.getLevel()) {
|| player.player.level() != access.getLevel()) {
player = brain.cachedPlayer = create(brain);
} else {
player.setState(access);

View File

@@ -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));
}
}
}

View File

@@ -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(),

View File

@@ -1,38 +1,37 @@
{
"parent": "block/block",
"textures": {
"particle": "computercraft:block/turtle_rainbow_overlay",
"texture": "computercraft:block/turtle_rainbow_overlay"
},
"elements": [
{
"name": "Flag",
"from": [1.5, 13, 10.5],
"to": [2, 16.5, 15.5],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [0, 0, 1, 6], "texture": "#texture"},
"east": {"uv": [0, 0, 7, 6], "texture": "#texture"},
"south": {"uv": [0, 0, 1, 6], "texture": "#texture"},
"west": {"uv": [0, 0, 7, 6], "texture": "#texture"},
"up": {"uv": [15, 0, 16, 6], "texture": "#texture"},
"down": {"uv": [8, 0, 9, 6], "texture": "#texture"}
}
},
{
"name": "Stick",
"from": [1.5, 10.5, 10.5],
"to": [2, 13, 11],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"east": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"south": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"west": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"up": {"uv": [12, 6, 13, 7], "texture": "#texture"},
"down": {"uv": [13, 6, 14, 7], "texture": "#texture"}
}
}
],
"display": {}
"parent": "block/block",
"textures": {
"particle": "computercraft:block/turtle_rainbow_overlay",
"texture": "computercraft:block/turtle_rainbow_overlay"
},
"elements": [
{
"name": "Flag",
"from": [1.5, 13.5, 10.5],
"to": [2, 16.5, 15.5],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [0, 0, 1, 6], "texture": "#texture"},
"east": {"uv": [0, 0, 7, 6], "texture": "#texture"},
"south": {"uv": [0, 0, 1, 6], "texture": "#texture"},
"west": {"uv": [0, 0, 7, 6], "texture": "#texture"},
"up": {"uv": [15, 0, 16, 6], "texture": "#texture"},
"down": {"uv": [8, 0, 9, 6], "texture": "#texture"}
}
},
{
"name": "Stick",
"from": [1.5, 10.5, 10.5],
"to": [2, 13.5, 11],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"east": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"south": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"west": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"up": {"uv": [12, 6, 13, 7], "texture": "#texture"},
"down": {"uv": [13, 6, 14, 7], "texture": "#texture"}
}
}
]
}

View File

@@ -1,38 +1,37 @@
{
"parent": "block/block",
"textures": {
"particle": "computercraft:block/turtle_trans_overlay",
"texture": "computercraft:block/turtle_trans_overlay"
},
"elements": [
{
"name": "Flag",
"from": [1.5, 13.5, 10.5],
"to": [2, 16.5, 15.5],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [0, 0, 1, 5], "texture": "#texture"},
"east": {"uv": [0, 0, 7, 5], "texture": "#texture"},
"south": {"uv": [0, 0, 1, 5], "texture": "#texture"},
"west": {"uv": [0, 0, 7, 5], "texture": "#texture"},
"up": {"uv": [15, 0, 16, 5], "texture": "#texture"},
"down": {"uv": [15, 0, 16, 5], "texture": "#texture"}
}
},
{
"name": "Stick",
"from": [1.5, 10.5, 10.5],
"to": [2, 13.5, 11],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"east": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"south": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"west": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"up": {"uv": [12, 6, 13, 7], "texture": "#texture"},
"down": {"uv": [13, 6, 14, 7], "texture": "#texture"}
}
}
],
"display": {}
"parent": "block/block",
"textures": {
"particle": "computercraft:block/turtle_trans_overlay",
"texture": "computercraft:block/turtle_trans_overlay"
},
"elements": [
{
"name": "Flag",
"from": [1.5, 13.5, 10.5],
"to": [2, 16, 15.5],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [0, 0, 1, 5], "texture": "#texture"},
"east": {"uv": [0, 0, 7, 5], "texture": "#texture"},
"south": {"uv": [0, 0, 1, 5], "texture": "#texture"},
"west": {"uv": [0, 0, 7, 5], "texture": "#texture"},
"up": {"uv": [15, 0, 16, 5], "texture": "#texture"},
"down": {"uv": [15, 0, 16, 5], "texture": "#texture"}
}
},
{
"name": "Stick",
"from": [1.5, 10.5, 10.5],
"to": [2, 13.5, 11],
"rotation": {"angle": 22.5, "axis": "x", "origin": [2, 11, 10.75]},
"faces": {
"north": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"east": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"south": {"uv": [12, 0, 13, 6], "texture": "#texture"},
"west": {"uv": [13, 0, 14, 6], "texture": "#texture"},
"up": {"uv": [12, 6, 13, 7], "texture": "#texture"},
"down": {"uv": [13, 6, 14, 7], "texture": "#texture"}
}
}
]
}

View File

@@ -27,6 +27,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.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
@@ -40,8 +41,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.*
@@ -656,6 +656,43 @@ 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.contents,
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(TurtleUpgrades.instance().get("minecraft:diamond_pickaxe"), turtle.getUpgrade(TurtleSide.LEFT))
}
}
/**
* Render turtles as an item.
*/

View 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}"
]
}

View 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}"
]
}

View File

@@ -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();
}

View File

@@ -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)
);

View File

@@ -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;

View File

@@ -175,9 +175,9 @@ public class OSAPI implements ILuaAPI {
}
/**
* Sets an alarm that will fire at the specified in-game time. When it
* fires, * an {@code alarm} event will be added to the event queue with the
* ID * returned from this function as the first parameter.
* Sets an alarm that will fire at the specified {@linkplain #time(IArguments) in-game time}.
* When it fires, an {@code alarm} event will be added to the event queue with the
* ID returned from this function as the first parameter.
*
* @param time The time at which to fire the alarm, in the range [0.0, 24.0).
* @return The ID of the new alarm. This can be used to filter the

View File

@@ -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() {
}
}

View File

@@ -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() {

View File

@@ -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();
}

View File

@@ -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.
*/

View File

@@ -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;
}
}

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.core.computer.computerthread;
import com.google.common.annotations.VisibleForTesting;
import com.google.errorprone.annotations.Keep;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.computer.TimeoutState;
@@ -774,6 +775,7 @@ public final class ComputerThread implements ComputerScheduler {
/**
* The current state of this worker.
*/
@Keep
private volatile ExecutorState $state = ExecutorState.IDLE;
/**
@@ -784,6 +786,7 @@ public final class ComputerThread implements ComputerScheduler {
* {@linkplain #afterWork()} finishes executing, we set this back to null and compute the difference between the
* two, updating the {@link Metrics#JAVA_ALLOCATION} metric.
*/
@Keep
private volatile @Nullable ThreadAllocation $threadAllocation = null;
/**

View File

@@ -8,6 +8,7 @@ import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.util.concurrent.TimeUnit;
@@ -82,6 +83,24 @@ public abstract class ArchiveMount<T extends ArchiveMount.FileEntry<T>> extends
*/
protected abstract byte[] getFileContents(String path, T file) throws IOException;
/**
* Convert an absolute path to one relative to {@code root}. If this path is not a child of {@code root}, return
* {@code null}.
*
* @param path The full path.
* @param root The root directory to be relative to.
* @return The relativised path, or {@code null}.
*/
protected static @Nullable String getLocalPath(String path, String root) {
// Some packs seem to include files not under the root, so drop them immediately.
if (!path.startsWith(root)) return null;
if (path.length() == root.length()) return "";
if (path.charAt(root.length()) != '/') return null;
return path.substring(root.length() + 1);
}
protected static class FileEntry<T extends FileEntry<T>> extends AbstractInMemoryMount.FileEntry<T> {
long size = -1;
}

View File

@@ -224,11 +224,11 @@ public class FileSystem {
public synchronized void copy(String sourcePath, String destPath) throws FileSystemException {
sourcePath = sanitizePath(sourcePath);
destPath = sanitizePath(destPath);
if (isReadOnly(destPath)) throw new FileSystemException("/" + destPath + ": " + ACCESS_DENIED);
if (!exists(sourcePath)) throw new FileSystemException("/" + sourcePath + ": " + NO_SUCH_FILE);
if (exists(destPath)) throw new FileSystemException("/" + destPath + ": " + FILE_EXISTS);
if (isReadOnly(destPath)) throw new FileSystemException(destPath, ACCESS_DENIED);
if (!exists(sourcePath)) throw new FileSystemException(sourcePath, NO_SUCH_FILE);
if (exists(destPath)) throw new FileSystemException(destPath, FILE_EXISTS);
if (contains(sourcePath, destPath)) {
throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself");
throw new FileSystemException(sourcePath, "Can't copy a directory inside itself");
}
copyRecursive(sourcePath, getMount(sourcePath), destPath, getMount(destPath), 0);
}
@@ -341,7 +341,7 @@ public class FileSystem {
}
}
if (match == null) {
throw new FileSystemException("/" + path + ": Invalid Path");
throw new FileSystemException(path, "Invalid Path");
}
return match;
}
@@ -404,7 +404,7 @@ public class FileSystem {
return String.join("/", outputParts);
}
public static boolean contains(String pathA, String pathB) {
private static boolean contains(String pathA, String pathB) {
pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT);
pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT);
@@ -421,7 +421,7 @@ public class FileSystem {
}
}
public static String toLocal(String path, String location) {
static String toLocal(String path, String location) {
path = sanitizePath(path);
location = sanitizePath(location);

View File

@@ -13,8 +13,12 @@ public class FileSystemException extends Exception {
@Serial
private static final long serialVersionUID = -2500631644868104029L;
FileSystemException(String s) {
super(s);
FileSystemException(String message) {
super(message);
}
FileSystemException(String path, String message) {
this("/" + path + ": " + message);
}
public static FileSystemException of(IOException e) {

View File

@@ -49,10 +49,9 @@ public final class JarMount extends ArchiveMount<JarMount.FileEntry> implements
while (zipEntries.hasMoreElements()) {
var entry = zipEntries.nextElement();
var entryPath = entry.getName();
if (!entryPath.startsWith(subPath)) continue;
var localPath = getLocalPath(entry.getName(), subPath);
if (localPath == null) continue;
var localPath = FileSystem.toLocal(entryPath, subPath);
getOrCreateChild(root, localPath, x -> new FileEntry()).setup(entry);
}
}

View File

@@ -129,7 +129,7 @@ class MountWrapper {
}
public void makeDirectory(String path) throws FileSystemException {
if (writableMount == null) throw exceptionOf(path, ACCESS_DENIED);
if (writableMount == null) throw new FileSystemException(path, ACCESS_DENIED);
path = toLocal(path);
try {
@@ -140,7 +140,7 @@ class MountWrapper {
}
public void delete(String path) throws FileSystemException {
if (writableMount == null) throw exceptionOf(path, ACCESS_DENIED);
if (writableMount == null) throw new FileSystemException(path, ACCESS_DENIED);
path = toLocal(path);
try {
@@ -151,7 +151,7 @@ class MountWrapper {
}
public void rename(String source, String dest) throws FileSystemException {
if (writableMount == null) throw exceptionOf(source, ACCESS_DENIED);
if (writableMount == null) throw new FileSystemException(source, ACCESS_DENIED);
source = toLocal(source);
dest = toLocal(dest);
@@ -168,7 +168,7 @@ class MountWrapper {
}
public SeekableByteChannel openForWrite(String path, Set<OpenOption> options) throws FileSystemException {
if (writableMount == null) throw exceptionOf(path, ACCESS_DENIED);
if (writableMount == null) throw new FileSystemException(path, ACCESS_DENIED);
path = toLocal(path);
try {
@@ -206,10 +206,6 @@ class MountWrapper {
private FileSystemException localExceptionOf(String path, String message) {
if (!location.isEmpty()) path = path.isEmpty() ? location : location + "/" + path;
return exceptionOf(path, message);
}
private static FileSystemException exceptionOf(String path, String message) {
return new FileSystemException("/" + path + ": " + message);
return new FileSystemException(path, message);
}
}

View File

@@ -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.

View File

@@ -1,3 +1,15 @@
# 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 modems not moving with Create contraptions.
# New features in CC: Tweaked 1.111.0
* Update several translations (Ale32bit).

View File

@@ -1,13 +1,13 @@
New features in CC: Tweaked 1.111.0
New features in CC: Tweaked 1.112.0
* Update several translations (Ale32bit).
* Split up turtle textures into individual textures.
* Add `r+`/`w+` support to the `io` library.
* Warn when capabilities are not registered and Optifine is installed.
* Report a custom error when using `!` instead of `not`.
* Update several translations (zyxkad, MineKID-LP).
* Add `cc.strings.split` function.
Several bug fixes:
* Allow planks to be used for building in "adventure" (dan200).
* Fix `disk.getAudioTitle()` returning untranslated strings for some modded discs.
* Fix crash when right clicking turtles in spectator.
* 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 modems not moving with Create contraptions.
Type "help changelog" to see the full version history.

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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")

View File

@@ -13,6 +13,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.network.client.UpgradesLoadedMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
@@ -34,6 +35,7 @@ import net.fabricmc.fabric.api.loot.v2.LootTableEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
@@ -119,6 +121,8 @@ public class ComputerCraft {
ComputerCraftAPI.registerGenericSource(new InventoryMethods());
Peripherals.addGenericLookup((world, pos, state, blockEntity, side, invalidate) -> InventoryMethods.extractContainer(world, pos, state, blockEntity, side));
if (FabricLoader.getInstance().isModLoaded(CreateIntegration.ID)) CreateIntegration.setup();
}
private record ReloadListener(String name, PreparableReloadListener listener)

View File

@@ -27,7 +27,7 @@
"dan200.computercraft.data.FabricDataGenerators"
],
"jei_mod_plugin": [
"dan200.computercraft.shared.integration.jei.JEIComputerCraft"
"dan200.computercraft.client.integration.jei.JEIComputerCraft"
],
"rei_client": [
"dan200.computercraft.client.integration.rei.REIComputerCraft"

View File

@@ -121,6 +121,11 @@ dependencies {
libs.bundles.externalMods.forge.compile.get().map { compileOnly(fg.deobf(it)) }
libs.bundles.externalMods.forge.runtime.get().map { runtimeOnly(fg.deobf(it)) }
// fg.debof only accepts a closure to configure the dependency, so doesn't work with Kotlin. We create and configure
// the dep first, and then pass it off to ForgeGradle.
(create(variantOf(libs.create.forge) { classifier("slim") }.get()) as ExternalModuleDependency)
.apply { isTransitive = false }.let { compileOnly(fg.deobf(it)) }
// Depend on our other projects.
api(commonClasses(project(":forge-api"))) { cct.exclude(this) }
clientApi(clientClasses(project(":forge-api"))) { cct.exclude(this) }

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