1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-17 06:57:38 +00:00

Compare commits

...

54 Commits

Author SHA1 Message Date
Jonathan Coates
0a8d505323 Bump CC:T to 1.115.0 2025-02-14 20:20:30 +00:00
Jonathan Coates
237a0ac3bb Expose printout contents to the API
Closes #2099
2025-02-14 18:13:20 +00:00
Jonathan Coates
b185d088b3 Suggest alternative table keys on nil errors (#2097)
We now suggest alternative table keys when code errors with "attempt
to index/call 'foo' (a nil value)". For example: "redstone.getinput()",
will now suggest "Did you mean: getInput".

This is a bit tricky to get right! In the above example, our code reads
like:

   1    GETTABUP 0 0 0 ; r0 := _ENV["redstone"]
   2    GETFIELD 0 0 1 ; r0 := r0["getinput"]
   3    CALL 0 1 1     ; r0()

Note, that when we get to the problematic line, we don't have access to
the original table that we attempted to index. In order to do this, we
borrow ideas from Lua's getobjname — we effectively write an evaluator
that walks back over the code and tries to reconstruct the expression
that resulted in nil.

For example, in the above case:
 - We know an instruction happened at pc=3, so we try to find the
   expression that computed r0.
 - We know this was set at pc=2, so we step back one. This is a GETFIELD
   instruction, so we check the key (it's a constant, so worth
   reporting), and then try to evaluate the table.
 - This version of r0 was set at pc=1, so we step back again. It's a
   GETTABUP instruction, so we can just evaluate that directly.

We then use this information (indexing _ENV.redstone with "getinput") to
find alternative keys (e.g. getInput, getOutput, etc...) and then pick
some likely suggestions with Damerau-Levenshtein/OSD.

I'm not entirely thrilled by the implementation here. The core
interpretation logic is implemented in Java. Which is *fine*, but a)
feels a little cheaty and b) means we're limited to what Lua bytecode
can provide (for instance, we can't inspect outer functions, or list all
available names in scope). We obviously can expand the bytecode if
needed, but something we'd want to be careful with.

The alternative approach would be to handle all the parsing in
Lua. Unfortunately, this is quite hard to get right — I think we'd need
some lazy parsing strategy to avoid constructing the whole AST, while
still retaining all the scope information we need.

I don't know. We really could make this as complex as we like, and I
don't know what the right balance is. It'd be cool to detect patterns
like the following, but is it *useful*?

    local monitor = peripheral.wrap("left")
    monitor.write("Hello")
        -- ^ monitor is nil. Is there a peripheral to the left of the
        -- computer?

For now, the current approach feels the easiest, and should allow us to
prototype things and see what does/doesn't work.
2025-02-13 21:57:29 +00:00
Jonathan Coates
051c70a731 Propagate exceptions from parallel where possible (#2095)
In the original implementation of our prettier runtime errors (#1320), we
wrapped the errors thrown within parallel functions into an exception
object. This means the call-stack is available to the catching-code, and
so is able to report a pretty exception message.

Unfortunately, this was a breaking change, and so we had to roll that
back. Some people were pcalling the parallel function, and matching on
the result of the error.

This is a second attempt at this, using a technique I've affectionately
dubbed "magic throws". The parallel API is now aware of whether it is
being pcalled or not, and thus able to decide whether to wrap the error
into an exception or not:

 - Add a new `cc.internal.tiny_require` module. This is a tiny
   reimplementation of require, for use in our global APIs.

 - Add a new (global, in the debug registry) `cc_try_barrier` function.
   This acts as a marker function, and is used to store additional
   information about the current coroutine.

   Currently this stores the parent coroutine (used to walk the full call
   stack) and a cache of whether any `pcall`-like function is on the
   stack.

   Both `parallel` and `cc.internal.exception.try` add this function to
   the root of the call stack.

 - When an error occurs within `parallel`, we walk up the call stack,
   using `cc_try_barrier` to traverse up the parent coroutine's stack
   too. If we do not find any `pcall`-like functions, then we know the
   error is never intercepted by user code, and so its safe to throw a
   full exception.
2025-02-13 17:38:57 +00:00
Jonathan Coates
2e2f308ff3 Support placing pocket computers on lecterns (#2098)
This allows shift+clicking a pocket computer on to a lectern. These
computers can be right clicked, opening the no-term computer GUI.
Terminal contents is rendered in-world, and broadcast to everyone in
range.

 - Add a new lectern PocketHolder.
 - Refactor some of the `PocketItemComputer` code to allow ticking pocket
   computers from a non-player/entity source.
 - Add a new model for pocket computers. This requires several new
   textures (somewhat mirroring the item ones), which is a little
   unfortunate, but looks much better than reusing the map renderer or
   item form.
2025-02-13 17:31:08 +00:00
Jonathan Coates
0f123b5efd Ignore unrepresentable characters when typing
In 94ad6dab0e, we changed it so typing
characters outside of CC's codepage were replaced with '?' rather than
ignored. This can be quite annoying for non-European users (where latin1
isn't very helpful!), so it makes sense to revert this change.

See discussion in #860 for more context.
2025-02-12 18:45:40 +00:00
Jonathan Coates
88cb03be6b Clean up the parallel API
- Store the filter alongside the coroutine rather than in a separate
   table (like we do in multishell).

 - Remove the redudant (I think!) second loop that checks for dead
   coroutines. We already check for dead coroutines in the main loop.

 - Rename some variables to be a bit more consistent. This makes this
   commit look noisier than it is. Sorry!
2025-02-09 16:53:59 +00:00
Jonathan Coates
4360485880 Bump CC:T to 1.114.4 2025-01-31 21:09:57 +00:00
Jonathan Coates
b69a44a927 Redo turtle move checks
Oh, this is so broken, and really has been since the 1.13 update, if not
earlier.

 - Fix call to isUnobstructed using the bounding box of the
   *destination* block rather than the turtle. This is almost always
   air, so the box is empty.

 - Because the above check has been wrong for so many years, we now
   significantly relax the "can push" checks for entities. We now allow
   pushing entities in any direction.

   We also remove the "isUnobstructed" check for the destination entity
   pos. This causes problems (if two entities are standing on a turtle,
   they'll obstruct each other), and given pistons don't perform such a
   check, I don't think we need it.

 - Also do a bit of cleanup around air/liquid checks. We often ended up
   reading the block state multiple times, which is a little ugly.
2025-01-27 22:45:52 +00:00
Jonathan Coates
7d8f609c49 literally sobbing rn 2025-01-26 22:27:50 +00:00
Jonathan Coates
e7f56c4d25 Remove ComponentMap
I'd originally kept this around to prevent mods that use CC internals,
but fa2140d00b has probably broken those
anyway, so let's not worry too much.
2025-01-26 15:13:24 +00:00
Jonathan Coates
fa2140d00b Clean up container data interface
- Remove ContainerData.open.

 - Change PlatformHelper.openMenu to take a separate display name and
   MenuConstructor, rather than a MenuProvider. This makes the interface
   slightly easier to use in the common case, where we want to use
   lambdas instead.
2025-01-26 14:56:09 +00:00
Jonathan Coates
03388149b1 Fix command computers being exposed as peripherals
- Check whether the computer is a command computer before registering
   the capability.

 - Add tests to check what is/isn't a peripheral. See also #2020, where
   we forgot to register a peripheral on NeoForge 1.21.1.

Fixes #2070.
2025-01-26 11:13:52 +00:00
Jonathan Coates
f212861370 Mark command computers as onlyOpCanSetNbt
This isn't required in vanilla, as the command computer is a
GameMasterBlock, and so isn't placeable in the first place.

*However*, this is a problem with Create contraptions — with those it's
possible to "place" a command computer complete with NBT. We override
onlyOpCanSetNbt to prevent this [^1].

[^1]: 7a7993deb8/src/main/java/com/simibubi/create/foundation/utility/NBTProcessors.java (L179)
2025-01-21 20:41:55 +00:00
Jonathan Coates
55edced9de Move GUI sprites to the sprites/ folder
This is where vanilla will read the sprites from in future versions, so
means we have a consistent layout between versions.

Also move the turtle "selected slot" texture to a sprite sheet. It would
be good to do more of these in the future (e.g. printer progress, maybe
bits of printouts).

Sorry to resource pack artists for causing trouble again.
2025-01-20 20:21:22 +00:00
Jonathan Coates
dc969c5a78 Configure ServerComputer via a Properties builder
We currently need to pass a whole bunch of arguments to a ServerComputer
in order to construct it, and if we implement #1814, this will get a
whole lot worse. Instead, we now pass most parameters (computer id,
family, label, term size, components) via a separate Properties class,
much like Minecraft does for blocks and items.

I'm not wild about the design of the API here, but I think it's a step
in the right direction.
2025-01-20 19:47:18 +00:00
Jonathan Coates
94ad6dab0e Map Unicode to CC's charset for char/paste events
We now convert uncode characters from "char" and "paste" events to CC's
charset[^1], rather than just leaving them unconverted. This means you
can paste in special characters like "♠" or "🮙" and they will be
converted correctly. Characters outside that range will be replaced with
"?", as before.

It would be nice to make this a bi-directional mapping, and do this for
Lua methods too (e.g. os.setComputerLabel). However, that has much wider
ramifications (and more likelyhood of breaking something), so avoiding
that for now.

 - Remove the generic "queue event" client->server message, and replace
   it with separate char/terminate/paste messages. This allows us to
   delete a chunk of code (all the NBT<->Object conversion), and makes
   server-side validation of events possible.

 - Fix os.setComputerLabel accepting the section sign — this is treated
   as special by Minecraft's formatting code. Sorry, no fun allowed.

 - Convert paste/char codepoints to CC's charset. Sadly MC's char hook
   splits the codepoint into surrogate pairs, which we *don't* attempt
   to reconstruct, so you can't currently use unicode input for block
   characters — you can paste them though!

[^1]: I'm referring this to the "terminal charset" within the code. I've
flip-flopped between "CraftOS", "terminal", "ComputerCraft", but feel
especially great.
2025-01-19 11:07:29 +00:00
Jonathan Coates
938eb38ad5 Move computer events to a single point
This abstraction never made much sense on InputHandler, as we only leave
the default methods on ServerComputer.

We now add a new class (ComputerEvents), which has a series of *static*
methods, that can queue an event on a ComputerEvents.Receiver object.
This is a bit of an odd indirection (why not just make them instance
methods on Receiver?!), but I don't really want those methods leaking
everywhere.
2025-01-18 19:26:10 +00:00
Jonathan Coates
6739c4c6c0 Wait for computers to run each tick in gametests 2025-01-17 18:43:19 +00:00
Jonathan Coates
d6749f8461 Set issue type in the templates
We already have the label, so it's not quite clear if it's worth it, but
let make our issue board look even more like a tube of smarties.
2025-01-17 17:25:39 +00:00
Jonathan Coates
5ba7f99326 Add back inputs on processResources
I kinda thought that Gradle would be smart enough to know that these
were input (given they're passed to expand), but apparently not :/.
2025-01-14 21:26:31 +00:00
Jonathan Coates
62c9e5b08f Add back missing translations
Lost in d3a3ab3c21, due to changes in
ea670cc358.
2025-01-14 18:14:51 +00:00
Jonathan Coates
2ca5850060 Bump CC:T to 1.114.3 2025-01-14 10:24:23 +00:00
Jonathan Coates
ed631b05e7 Fix TurtleTool using the wrong stack
When "placing" the item (e.g. hoeing soil), we were using the tool item,
rather than the passed stack. This was introduced in
9a914e75c4, so never made it into a
release.
2025-01-14 10:24:23 +00:00
Jonathan Coates
a2b9490d5c Some printer gametests
These are a little ugly, but do catch some issues we've seen in the
past. Fixes #1682 (for now), and on its birthday too!
2025-01-14 09:38:22 +00:00
Jonathan Coates
8204944b5f More Gradle cleanup
Mostly configuration cache support. And an aborted attempt at updating
spotless, which just resulted in a bunch of ktlint issues :/.
2025-01-14 08:48:48 +00:00
Jonathan Coates
ef0af67e96 Share some dependency exclusion code
- Disable Gradle module metadata for all Minecraft projects
 - Run dependency exclusion code for all projects

We need the former for MDG on 1.21, so might as well do some other
cleanup while we're here.
2025-01-13 00:02:54 +00:00
Jonathan Coates
9a914e75c4 Rethink PlatformHelper.useOn
useOn is now only responsible for firing the actual mod loader events,
and just returns the result of firing that event. The actual calling of
Block.use/Item.useOn now live in TurtlePlaceCommand.

This isn't especially useful for 1.20.1, but is more relevant on 1.21.1
when we look at #2011, as the shared code is much larger.
2025-01-12 22:01:43 +00:00
Jonathan Coates
546577041b More turtle game tests
See #1682. Mostly trying to capture some behaviour I know has changed in
1.21, so we can verify it more easily.
2025-01-12 20:29:37 +00:00
Jonathan Coates
f881c0ced0 A few more gametests, update to Gradle 8.12
Okay, listen. I started writing a few more gametests (see #1682), and
then thought I'd do a cheeky Gradle update. However, that broke
vanilla-extract[^1], and also triggered a load of deprecation warnings,
and at that point it was too late to separate the too.

[^1]: 8975ed5a7b
2025-01-12 18:26:51 +00:00
Jonathan Coates
0b389e04b0 Move Fabric datagen mixins to datagen module
This way they don't interfere with other mods doing datagen.

Also bump reuse version, to fix issues with globs.
2025-01-12 12:44:57 +00:00
Jonathan Coates
d3a3ab3c21 Sync translations from Crowdin
Closes #2044

Co-authored-by: Yaroslav Petryk <Yaroslav01@ukr.net>
2025-01-12 11:47:58 +00:00
Jonathan Coates
22e6c06e59 Don't store terminal snapshot as a ByteBuf
There's too many issues here around object pooling and reference
counting, that it's just not practical to correctly handle. We now just
use a plain old byte[] rather than a ByteBuf.

This does mean the array now lives on the Java heap rather than the
direct heap, but I *think* that's fine. It's something that is hard to
measure.

Fixes #2059
2025-01-12 10:37:27 +00:00
Jonathan Coates
3c46b8acd7 Clean up Javadocs a little
I've no motivation for modding right now, but always got time for build
system busywork!

CC:T (and CC before that) has always published its API docs. However,
they're not always the most helpful — they're useful if you know what
you're looking for, but aren't a good getting-started guide.

Part of the issue here is there's no examples, and everything is
described pretty abstractly. I have occasionally tried to improve this
(e.g. the peripheral docs in bdffabc08e),
but it's a long road.

This commit adds a new example mod, which registers peripherals, an API
and a turtle upgrade. While the mod itself isn't exported as part of the
docs, we reference blocks of it using Java's new {@snippet} tag.

 - Switch the Forge project to use NeoForge's new Legacy MDG plugin. We
   don't *need* to do this, but it means the build logic for Forge and
   NeoForge is more closely aligned.

 - Add a new SnippetTaglet, which is a partial backport of Java 18+'s
   {@snippet}.

 - Add an example mod. This is a working multi-loader mod, complete with
   datagen (albeit with no good multi-loader abstractions).

 - Move our existing <pre>{@code ...}</pre> blocks into the example mod,
   replacing them with {@snippet}s.

 - Add a new overview page to the docs, providing some getting-started
   information. We had this already in the dan200.computercraft.api
   package docs, but it's not especially visible there.
2025-01-09 20:47:51 +00:00
Jonathan Coates
d9fc1c3a80 Remove mention of submitting translations via PRs
Turns out it's possible, but a bit of a faff with CrowdIn. Why is all
translation software terrible!
2025-01-05 23:25:29 +00:00
Jonathan Coates
479aabdd09 Tweaks to CONTRIBUTING.md 2025-01-05 23:23:24 +00:00
Jonathan Coates
ad74893058 Remove redundant condition in io.read 2025-01-05 23:02:16 +00:00
Jonathan Coates
2ba6d5815b Fix fs.isDriveRoot for missing files
fs.getDrive returns nil for missing files, rather than the mount of the
parent file. This is a bit confusing — other mount-related functions
(e.g. getFreeSpace) find the nearest mount — but I think it's too late
to change this. Instead, we check if the file exists first.
2024-12-31 11:12:58 +00:00
tizu
7e2f490626 Show HTTP error in wget (#2037) 2024-12-22 16:45:04 +00:00
Jonathan Coates
4dc649d5e5 Merge pull request #2029 from MCJack123/patch-19
Add `turtle.getItemDetail` `detailed` flag introduction date
2024-12-12 09:18:44 +00:00
JackMacWindows
5bab415790 Add turtle.getItemDetail detailed flag introduction date 2024-12-12 00:44:25 -05:00
Jonathan Coates
9bbf3f3e1d Move datagen to its own source set
MC 1.21.4 means we have to move more data generation code into the
client source set. Given all this code movement, it probably makes sense
to put data generation in a separate source set instead.

1.21.4 also has split data generators for client and server, but neither
mod loader recommends this. This means we can/should merge DataProviders
and ClientDataProviders into a single class.

Data generators are no longer bundled with the jar, which does reduce
file size, but by a tiny amount (~70KiB).
2024-12-05 12:03:45 +00:00
Jonathan Coates
7c02979c22 Upload jars in CI earlier
This means they're uploaded even if the linters or tests fail.
2024-11-30 12:07:55 +00:00
Jonathan Coates
fdb65c9368 Link to monitor events from the main monitor page 2024-11-28 10:31:36 +00:00
Jonathan Coates
ea670cc358 Try to unify our config files a bit
I've tried so many rewrites of the config system over the last few
months, in an attempt to get started on #1727. All of them stink, so
this is an attempt to apply some of the cleanup.

 - Move some of the common logic into ConfigFile. This means we now
   store more information ourselves for Forge, rather than reading it
   out of the ForgeConfigSpec.

 - Don't include the Range/Allowed keys in the translation key. This was
   mostly there because of how we read comments from Forge, but it never
   made much sense.

 - Remove our separate Trie structure, and just encode the tree as part
   of the children of a Group.
2024-11-24 21:53:04 +00:00
Jonathan Coates
1963e0160f Bump CC:T to 1.114.2
Ahhhh, I hate graphics code.
2024-11-23 09:34:03 +00:00
Jonathan Coates
9a06904634 Only skip cursor if it exists
If the cursor is not visible then we'd end up blinking the last
character on the screen. And if the screen was empty we'd spew the logs
with GL errors.
2024-11-23 09:31:04 +00:00
Jonathan Coates
5e24ad17d7 Bump CC:T to 1.114.1 2024-11-23 09:14:53 +00:00
Jonathan Coates
8b1cb09ddf Update translations 2024-11-23 09:10:50 +00:00
Jonathan Coates
d1a6b043c2 Clean up monitor cursor rendering
- Use the correct index count for the cursor quad. Monitors are now
   rendered as quads, rather than triangles.
 - *Skip* rendering the cursor vertex, rather than additionally
   rendering it.

I confess, I'm baffled how this code was ever written. From what I can
tell, this has been broken since it was first introduced in
4228011b84, and I'm sure I tested it then.

Fixes #2013. Probably.
2024-11-23 08:24:11 +00:00
Jonathan Coates
cddb8fec11 Fix crash when lectern has no item
This should never happen in practice, but might happen when using
/setblock or after world corruption, so let's be careful here.

Closes #2014
2024-11-19 21:35:00 +00:00
Jonathan Coates
1d7d8006d4 Stop publishing to CurseForge
Been dragging my feet over this for a while now, but increasingly
uncomfortable with Overwolf. I'm not going to delete the project (or any
existing versions), just not publish any new versions there.
2024-11-16 15:19:00 +00:00
Jonathan Coates
63bdc2537c Fix monitor events using the wrong computer set
Fixes #2010
2024-11-15 18:43:28 +00:00
Jonathan Coates
f776b17150 Fix argument order of math.atan in changelog 2024-11-15 15:32:40 +00:00
255 changed files with 5644 additions and 2592 deletions

View File

@@ -18,11 +18,6 @@ ij_any_if_brace_force = if_multiline
ij_any_for_brace_force = if_multiline
ij_any_spaces_within_array_initializer_braces = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_method_parameters_wrap = off
ij_kotlin_call_parameters_wrap = off
[*.md]
trim_trailing_whitespace = false
@@ -31,3 +26,16 @@ indent_size = 2
[*.yml]
indent_size = 2
[{*.kt,*.kts}]
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_size = 4
ij_kotlin_spaces_around_equality_operators = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
# Prefer to handle these manually
ij_kotlin_method_parameters_wrap = off
ij_kotlin_call_parameters_wrap = off
ij_kotlin_extends_list_wrap = off

View File

@@ -1,6 +1,7 @@
name: Bug report
description: Report some misbehaviour in the mod
labels: [ bug ]
type: bug
body:
- type: dropdown
id: mc-version
@@ -29,3 +30,5 @@ body:
Description of the bug. Please include the following:
- Logs: These will be located in the `logs/` directory of your Minecraft instance. This is always useful, even if it doesn't include errors, so please upload this!
- Detailed reproduction steps: sometimes I can spot a bug pretty easily, but often it's much more obscure. The more information I have to help reproduce it, the quicker it'll get fixed.
![A gif of burning text reading "Upload your logs!!!"](https://tweaked.cc/images/logs.gif)

View File

@@ -2,6 +2,7 @@
name: Feature request
about: Suggest an idea or improvement
labels: enhancement
type: feature
---
<!--

View File

@@ -30,6 +30,18 @@ jobs:
- name: ⚒️ Build
run: ./gradlew assemble || ./gradlew assemble
- name: 📦 Prepare Jars
run: |
# Find the main jar and append the git hash onto it.
mkdir -p jars
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
- name: 📤 Upload Jar
uses: actions/upload-artifact@v4
with:
name: CC-Tweaked
path: ./jars
- name: Cache pre-commit
uses: actions/cache@v4
with:
@@ -54,18 +66,6 @@ jobs:
run: ./tools/parse-reports.py
if: ${{ failure() }}
- name: 📦 Prepare Jars
run: |
# Find the main jar and append the git hash onto it.
mkdir -p jars
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
- name: 📤 Upload Jar
uses: actions/upload-artifact@v4
with:
name: CC-Tweaked
path: ./jars
build-core:
strategy:
fail-fast: false

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@
*.iml
.idea
.gradle
.kotlin
*.DS_Store
/.classpath

View File

@@ -27,7 +27,7 @@ repos:
exclude: "^(.*\\.(bat)|LICENSE)$"
- repo: https://github.com/fsfe/reuse-tool
rev: v4.0.3
rev: v5.0.2
hooks:
- id: reuse
@@ -58,6 +58,7 @@ repos:
exclude: |
(?x)^(
projects/[a-z]+/src/generated|
projects/[a-z]+/src/examples/generatedResources|
projects/core/src/test/resources/test-rom/data/json-parsing/|
.*\.dfpwm
)

View File

@@ -22,8 +22,7 @@ If you have a bug, suggestion, or other feedback, the best thing to do is [file
use the issue templates - they provide a useful hint on what information to provide.
## Translations
Translations are managed through [CrowdIn], an online interface for managing language strings. Translations may either
be contributed there, or directly via a pull request.
Translations are managed through [CrowdIn], an online interface for managing language strings.
## Setting up a development environment
In order to develop CC: Tweaked, you'll need to download the source code and then run it.
@@ -49,9 +48,12 @@ If you want to run CC:T in a normal Minecraft instance, run `./gradlew assemble`
`projects/forge/build/libs` (for Forge) or `projects/fabric/build/libs` (for Fabric).
## Developing CC: Tweaked
Before making any major changes to CC: Tweaked, I'd recommend you have a read of the [the architecture
document][architecture] first. While it's not a comprehensive document, it gives a good hint of where you should start
looking to make your changes. As always, if you're not sure, [do ask the community][community]!
Before making any major changes to CC: Tweaked, I'd recommend starting opening an issue or starting a discussion on
GitHub first. It's often helpful to discuss features before spending time developing them!
Once you're ready to start programming, have a read of the [the architecture document][architecture] first. While it's
not a comprehensive document, it gives a good hint of where you should start looking to make your changes. As always, if
you're not sure, [do ask the community][community]!
### Testing
When making larger changes, it may be useful to write a test to make sure your code works as expected.

View File

@@ -11,14 +11,13 @@ SPDX-License-Identifier: MPL-2.0
</picture>
[![Current build status](https://github.com/cc-tweaked/CC-Tweaked/workflows/Build/badge.svg)](https://github.com/cc-tweaked/CC-Tweaked/actions "Current build status")
[![Download CC: Tweaked on CurseForge](https://img.shields.io/static/v1?label=Download&message=CC:%20Tweaked&color=E04E14&logoColor=E04E14&logo=CurseForge)][CurseForge]
[![Download CC: Tweaked on Modrinth](https://img.shields.io/static/v1?label=Download&color=00AF5C&logoColor=00AF5C&logo=Modrinth&message=CC:%20Tweaked)][Modrinth]
CC: Tweaked is a mod for Minecraft which adds programmable computers, turtles and more to the game. A fork of the
much-beloved [ComputerCraft], it continues its legacy with improved performance and stability, along with a wealth of
new features.
CC: Tweaked can be installed from [CurseForge] or [Modrinth]. It runs on both [Minecraft Forge] and [Fabric].
CC: Tweaked can be installed from [Modrinth]. It runs on both [Minecraft Forge] and [Fabric].
## Contributing
Any contribution is welcome, be that using the mod, reporting bugs or contributing code. If you want to get started
@@ -62,19 +61,6 @@ dependencies {
}
```
When using ForgeGradle, you may also need to add the following:
```groovy
minecraft {
runs {
configureEach {
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg"
}
}
}
```
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file
an issue to let me know!
@@ -83,7 +69,6 @@ We bundle the API sources with the jar, so documentation should be easily viewab
the generated documentation [can be browsed online](https://tweaked.cc/javadoc/).
[computercraft]: https://github.com/dan200/ComputerCraft "ComputerCraft on GitHub"
[curseforge]: https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked from CurseForge"
[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."

View File

@@ -8,18 +8,23 @@ 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 = [
# Generated/data files are CC0.
"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/*/src/generated/**",
"projects/web/src/htmlTransform/export/index.json",
"projects/web/src/htmlTransform/export/items/minecraft/**",
# 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.
".github/**",
# Example mod is CC0.
"projects/*/src/examples/**"
]
[[annotations]]
@@ -30,23 +35,15 @@ path = [
"doc/images/**",
"package.json",
"package-lock.json",
"projects/common/src/client/resources/computercraft-client.mixins.json",
"projects/*/src/*/resources/*.mixins.json",
"projects/fabric/src/*/resources/fabric.mod.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",
@@ -73,7 +70,7 @@ path = [
]
[[annotations]]
# Community-contributed license files
# Community-contributed language files
SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers"
SPDX-License-Identifier = "LicenseRef-CCPL"
path = [
@@ -87,18 +84,11 @@ path = [
]
[[annotations]]
# Community-contributed license files
# Community-contributed language 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/**"]
SPDX-FileCopyrightText = "Gradle Inc"

View File

@@ -24,21 +24,19 @@ val mcVersion: String by extra
githubRelease {
token(findProperty("githubApiKey") as String? ?: "")
owner.set("cc-tweaked")
repo.set("CC-Tweaked")
targetCommitish.set(cct.gitBranch)
owner = "cc-tweaked"
repo = "CC-Tweaked"
targetCommitish = cct.gitBranch
tagName.set("v$mcVersion-$modVersion")
releaseName.set("[$mcVersion] $modVersion")
body.set(
provider {
"## " + project(":core").file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md")
.readLines()
.takeWhile { it != "Type \"help changelog\" to see the full version history." }
.joinToString("\n").trim()
},
)
prerelease.set(isUnstable)
tagName = "v$mcVersion-$modVersion"
releaseName = "[$mcVersion] $modVersion"
body = provider {
"## " + project(":core").file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md")
.readLines()
.takeWhile { it != "Type \"help changelog\" to see the full version history." }
.joinToString("\n").trim()
}
prerelease = isUnstable
}
tasks.publish { dependsOn(tasks.githubRelease) }
@@ -118,7 +116,7 @@ idea.project.settings.compiler.javac {
}
versionCatalogUpdate {
sortByKey.set(false)
sortByKey = false
pin { versions.addAll("fastutil", "guava", "netty", "slf4j") }
keep { keepUnusedLibraries.set(true) }
keep { keepUnusedLibraries = true }
}

View File

@@ -14,18 +14,10 @@ repositories {
mavenCentral()
gradlePluginPortal()
maven("https://maven.minecraftforge.net") {
name = "Forge"
maven("https://maven.neoforged.net") {
name = "NeoForge"
content {
includeGroup("net.minecraftforge")
includeGroup("net.minecraftforge.gradle")
}
}
maven("https://maven.parchmentmc.org") {
name = "Librarian"
content {
includeGroupByRegex("^org\\.parchmentmc.*")
includeGroup("net.neoforged")
}
}
@@ -49,12 +41,10 @@ dependencies {
implementation(libs.kotlin.plugin)
implementation(libs.spotless)
implementation(libs.curseForgeGradle)
implementation(libs.fabric.loom)
implementation(libs.forgeGradle)
implementation(libs.ideaExt)
implementation(libs.librarian)
implementation(libs.minotaur)
implementation(libs.modDevGradle)
implementation(libs.vanillaExtract)
}
@@ -78,7 +68,7 @@ gradlePlugin {
}
versionCatalogUpdate {
sortByKey.set(false)
keep { keepUnusedLibraries.set(true) }
catalogFile.set(file("../gradle/libs.versions.toml"))
sortByKey = false
keep { keepUnusedLibraries = true }
catalogFile = file("../gradle/libs.versions.toml")
}

View File

@@ -30,7 +30,7 @@ repositories {
loom {
splitEnvironmentSourceSets()
splitModDependencies.set(true)
splitModDependencies = true
}
MinecraftConfigurations.setup(project)

View File

@@ -10,21 +10,22 @@ import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("net.minecraftforge.gradle")
// We must apply java-convention after Forge, as we need the fg extension to be present.
id("cc-tweaked.java-convention")
id("org.parchmentmc.librarian.forgegradle")
id("net.neoforged.moddev.legacyforge")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
minecraft {
legacyForge {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
mappings("parchment", "${libs.findVersion("parchmentMc").get()}-${libs.findVersion("parchment").get()}-$mcVersion")
version = "${mcVersion}-${libs.findVersion("forge").get()}"
accessTransformer(project(":forge").file("src/main/resources/META-INF/accesstransformer.cfg"))
parchment {
minecraftVersion = libs.findVersion("parchmentMc").get().toString()
mappingsVersion = libs.findVersion("parchment").get().toString()
}
}
MinecraftConfigurations.setup(project)
@@ -32,13 +33,3 @@ MinecraftConfigurations.setup(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = "forge")
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
"minecraft"("net.minecraftforge:forge:$mcVersion-${libs.findVersion("forge").get()}")
}
tasks.configureEach {
// genIntellijRuns isn't registered until much later, so we need this silly hijinks.
if (name == "genIntellijRuns") doLast { IdeaRunConfigurations(project).patch() }
}

View File

@@ -29,7 +29,7 @@ base.archivesName.convention("cc-tweaked-$mcVersion-${project.name}")
java {
toolchain {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
languageVersion= CCTweakedPlugin.JAVA_VERSION
}
withSourcesJar()
@@ -44,13 +44,6 @@ repositories {
exclusiveContent {
forRepositories(mainMaven)
// Include the ForgeGradle repository if present. This requires that ForgeGradle is already present, which we
// enforce in our Forge overlay.
val fg =
project.extensions.findByType(net.minecraftforge.gradle.userdev.DependencyManagementExtension::class.java)
if (fg != null) forRepositories(fg.repository)
filter {
includeGroup("cc.tweaked")
// Things we mirror
@@ -99,6 +92,7 @@ sourceSets.all {
check("OperatorPrecedence", CheckSeverity.OFF) // For now.
check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid
check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
check("InvalidInlineTag", CheckSeverity.OFF) // Triggered by @snippet. Can be removed on Java 21.
check("NullAway", CheckSeverity.ERROR)
option(
@@ -169,8 +163,8 @@ tasks.test {
}
tasks.withType(JacocoReport::class.java).configureEach {
reports.xml.required.set(true)
reports.html.required.set(true)
reports.xml.required = true
reports.html.required =true
}
project.plugins.withType(CCTweakedPlugin::class.java) {
@@ -226,6 +220,5 @@ idea.module {
// Force Gradle to write to inherit the output directory from the parent, instead of writing to out/xxx/classes.
// This is required for Loom, and we patch Forge's run configurations to work there.
// TODO: Submit a patch to Forge to support ProjectRootManager.
inheritOutputDirs = true
}

View File

@@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.CCTweakedPlugin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
}
kotlin {
jvmToolchain {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
}
}
tasks.withType(KotlinCompile::class.java).configureEach {
// So technically we shouldn't need to do this as the toolchain sets it above. However, the option only appears
// to be set when the task executes, so doesn't get picked up by IDEs.
kotlinOptions.jvmTarget = when {
CCTweakedPlugin.JAVA_VERSION.asInt() > 8 -> CCTweakedPlugin.JAVA_VERSION.toString()
else -> "1.${CCTweakedPlugin.JAVA_VERSION.asInt()}"
}
}

View File

@@ -2,11 +2,9 @@
//
// SPDX-License-Identifier: MPL-2.0
import net.darkhax.curseforgegradle.TaskPublishCurseForge
import cc.tweaked.gradle.setProvider
plugins {
id("net.darkhax.curseforgegradle")
id("com.modrinth.minotaur")
id("cc-tweaked.publishing")
}
@@ -25,34 +23,17 @@ val isUnstable = project.properties["isUnstable"] == "true"
val modVersion: String by extra
val mcVersion: String by extra
val publishCurseForge by tasks.registering(TaskPublishCurseForge::class) {
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Upload artifacts to CurseForge"
apiToken = findProperty("curseForgeApiKey") ?: ""
enabled = apiToken != ""
val mainFile = upload("282001", modPublishing.output)
mainFile.changelog =
"Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion)."
mainFile.changelogType = "markdown"
mainFile.releaseType = if (isUnstable) "alpha" else "release"
mainFile.gameVersions.add(mcVersion)
}
tasks.publish { dependsOn(publishCurseForge) }
modrinth {
token.set(findProperty("modrinthApiKey") as String? ?: "")
projectId.set("gu7yAYhd")
versionNumber.set(modVersion)
versionName.set(modVersion)
versionType.set(if (isUnstable) "alpha" else "release")
token = findProperty("modrinthApiKey") as String? ?: ""
projectId = "gu7yAYhd"
versionNumber = modVersion
versionName = modVersion
versionType = if (isUnstable) "alpha" else "release"
uploadFile.setProvider(modPublishing.output)
gameVersions.add(mcVersion)
changelog.set("Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion).")
changelog = "Release notes can be found on the [GitHub repository](https://github.com/cc-tweaked/CC-Tweaked/releases/tag/v$mcVersion-$modVersion)."
syncBodyFrom.set(provider { rootProject.file("doc/mod-page.md").readText() })
syncBodyFrom = provider { rootProject.file("doc/mod-page.md").readText() }
}
tasks.publish { dependsOn(tasks.modrinth) }

View File

@@ -2,44 +2,34 @@
//
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
/**
* Sets up the configurations for writing game tests.
*
* See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas.
*/
import cc.tweaked.gradle.MinecraftConfigurations
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
plugins {
id("cc-tweaked.kotlin-convention")
kotlin("jvm")
id("cc-tweaked.java-convention")
}
val main = sourceSets["main"]
val client = sourceSets["client"]
// Both testMod and testFixtures inherit from the main and client classpath, just so we have access to Minecraft classes.
val testMod by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
}
MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.DATAGEN)
MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.EXAMPLES)
MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.TEST_MOD)
configurations {
named(testMod.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(compileClasspath.get())
}
// Set up generated resources
sourceSets.main { resources.srcDir("src/generated/resources") }
sourceSets.named("examples") { resources.srcDir("src/examples/generatedResources") }
named(testMod.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(runtimeClasspath.get())
}
}
// Like the main test configurations, we're safe to depend on source set outputs.
dependencies {
add(testMod.implementationConfigurationName, main.output)
add(testMod.implementationConfigurationName, client.output)
}
// Make sure our examples compile.
tasks.check { dependsOn(tasks.named("compileExamplesJava")) }
// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath.

View File

@@ -12,25 +12,26 @@ publishing {
register<MavenPublication>("maven") {
artifactId = base.archivesName.get()
from(components["java"])
suppressAllPomMetadataWarnings()
pom {
name.set("CC: Tweaked")
description.set("CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles and more to Minecraft.")
url.set("https://github.com/cc-tweaked/CC-Tweaked")
name = "CC: Tweaked"
description = "CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles and more to Minecraft."
url = "https://github.com/cc-tweaked/CC-Tweaked"
scm {
url.set("https://github.com/cc-tweaked/CC-Tweaked.git")
url = "https://github.com/cc-tweaked/CC-Tweaked.git"
}
issueManagement {
system.set("github")
url.set("https://github.com/cc-tweaked/CC-Tweaked/issues")
system = "github"
url = "https://github.com/cc-tweaked/CC-Tweaked/issues"
}
licenses {
license {
name.set("ComputerCraft Public License, Version 1.0")
url.set("https://github.com/cc-tweaked/CC-Tweaked/blob/HEAD/LICENSE")
name = "ComputerCraft Public License, Version 1.0"
url = "https://github.com/cc-tweaked/CC-Tweaked/blob/HEAD/LICENSE"
}
}
}

View File

@@ -10,14 +10,9 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.attributes.TestSuiteType
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.reporting.ReportingExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
@@ -25,7 +20,6 @@ import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.process.JavaForkOptions
import org.gradle.testing.jacoco.plugins.JacocoCoverageReport
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.gradle.testing.jacoco.tasks.JacocoReport
@@ -36,54 +30,40 @@ import java.io.IOException
import java.net.URI
import java.util.regex.Pattern
abstract class CCTweakedExtension(
private val project: Project,
private val fs: FileSystemOperations,
) {
abstract class CCTweakedExtension(private val project: Project) {
/** Get the hash of the latest git commit. */
val gitHash: Provider<String> = gitProvider(project, "<no git hash>") {
ProcessHelpers.captureOut("git", "-C", project.rootProject.projectDir.absolutePath, "rev-parse", "HEAD").trim()
}
val gitHash: Provider<String> =
gitProvider("<no git commit>", listOf("rev-parse", "HEAD")) { it.trim() }
/** Get the current git branch. */
val gitBranch: Provider<String> = gitProvider(project, "<no git branch>") {
ProcessHelpers.captureOut("git", "-C", project.rootProject.projectDir.absolutePath, "rev-parse", "--abbrev-ref", "HEAD")
.trim()
}
val gitBranch: Provider<String> =
gitProvider("<no git branch>", listOf("rev-parse", "--abbrev-ref", "HEAD")) { it.trim() }
/** Get a list of all contributors to the project. */
val gitContributors: Provider<List<String>> = gitProvider(project, listOf()) {
ProcessHelpers.captureLines(
"git", "-C", project.rootProject.projectDir.absolutePath, "shortlog", "-ns",
"--group=author", "--group=trailer:co-authored-by", "HEAD",
)
.asSequence()
.map {
val matcher = COMMIT_COUNTS.matcher(it)
matcher.find()
matcher.group(1)
}
.filter { !IGNORED_USERS.contains(it) }
.toList()
.sortedWith(String.CASE_INSENSITIVE_ORDER)
}
val gitContributors: Provider<List<String>> =
gitProvider(listOf(), listOf("shortlog", "-ns", "--group=author", "--group=trailer:co-authored-by", "HEAD")) { input ->
input.lineSequence()
.filter { it.isNotEmpty() }
.map {
val matcher = COMMIT_COUNTS.matcher(it)
matcher.find()
matcher.group(1)
}
.filter { !IGNORED_USERS.contains(it) }
.toList()
.sortedWith(String.CASE_INSENSITIVE_ORDER)
}
/**
* References to other sources
*/
val sourceDirectories: SetProperty<SourceSetReference> = project.objects.setProperty(SourceSetReference::class.java)
/**
* Dependencies excluded from published artifacts.
*/
private val excludedDeps: ListProperty<Dependency> = project.objects.listProperty(Dependency::class.java)
/** All source sets referenced by this project. */
val sourceSets = sourceDirectories.map { x -> x.map { it.sourceSet } }
init {
sourceDirectories.finalizeValueOnRead()
excludedDeps.finalizeValueOnRead()
project.afterEvaluate { sourceDirectories.disallowChanges() }
}
@@ -109,14 +89,13 @@ abstract class CCTweakedExtension(
val otherJava = otherProject.extensions.getByType(JavaPluginExtension::class.java)
val main = otherJava.sourceSets.getByName("main")
val client = otherJava.sourceSets.getByName("client")
val testMod = otherJava.sourceSets.findByName("testMod")
val testFixtures = otherJava.sourceSets.findByName("testFixtures")
// Pull in sources from the other project.
extendSourceSet(otherProject, main)
extendSourceSet(otherProject, client)
if (testMod != null) extendSourceSet(otherProject, testMod)
if (testFixtures != null) extendSourceSet(otherProject, testFixtures)
for (sourceSet in listOf(MinecraftConfigurations.DATAGEN, MinecraftConfigurations.EXAMPLES, MinecraftConfigurations.TEST_MOD, "testFixtures")) {
otherJava.sourceSets.findByName(sourceSet)?.let { extendSourceSet(otherProject, it) }
}
// The extra source-processing tasks should include these files too.
project.tasks.named(main.javadocTaskName, Javadoc::class.java) { source(main.allJava, client.allJava) }
@@ -179,23 +158,19 @@ 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.capitalise()}Report"
val jacoco = project.extensions.getByType(JacocoPluginExtension::class.java)
task.configure {
finalizedBy(reportTaskName)
doFirst("Clean class dump directory") { fs.delete { delete(classDump) } }
jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
classDumpDir = classDump.get().asFile
// Older versions of modlauncher don't include a protection domain (and thus no code
// source). Jacoco skips such classes by default, so we need to explicitly include them.
isIncludeNoLocationClasses = true
excludes = listOf(
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them.
)
}
}
@@ -204,15 +179,11 @@ abstract class CCTweakedExtension(
description = "Generates code coverage report for the ${task.name} task."
executionData(task.get())
classDirectories.from(classDump)
// Don't want to use sourceSets(...) here as we have a custom class directory.
for (ref in sourceSets.get()) sourceDirectories.from(ref.allSource.sourceDirectories)
}
project.extensions.configure(ReportingExtension::class.java) {
reports.register("${task.name}CodeCoverageReport", JacocoCoverageReport::class.java) {
testType.set(TestSuiteType.INTEGRATION_TEST)
// Don't want to use sourceSets(...) here as we don't use all class directories.
for (ref in this@CCTweakedExtension.sourceDirectories.get()) {
sourceDirectories.from(ref.sourceSet.allSource.sourceDirectories)
if (ref.classes) classDirectories.from(ref.sourceSet.output)
}
}
}
@@ -252,18 +223,23 @@ abstract class CCTweakedExtension(
).resolve().single()
}
/**
* Exclude a dependency from being published in Maven.
*/
fun exclude(dep: Dependency) {
excludedDeps.add(dep)
}
private fun <T> gitProvider(default: T, command: List<String>, process: (String) -> T): Provider<T> {
val baseResult = project.providers.exec {
commandLine = listOf("git", "-C", project.rootDir.absolutePath) + command
}
/**
* Configure a [MavenDependencySpec].
*/
fun configureExcludes(spec: MavenDependencySpec) {
for (dep in excludedDeps.get()) spec.exclude(dep)
return project.provider {
val res = try {
baseResult.standardOutput.asText.get()
} catch (e: IOException) {
project.logger.error("Cannot read Git repository: ${e.message}", e)
return@provider default
} catch (e: GradleException) {
project.logger.error("Cannot read Git repository: ${e.message}", e)
return@provider default
}
process(res)
}
}
companion object {
@@ -272,20 +248,6 @@ abstract class CCTweakedExtension(
"GitHub", "Daniel Ratcliffe", "NotSquidDev", "Weblate",
)
private fun <T> gitProvider(project: Project, default: T, supplier: () -> T): Provider<T> {
return project.provider {
try {
supplier()
} catch (e: IOException) {
project.logger.error("Cannot read Git repository: ${e.message}")
default
} catch (e: GradleException) {
project.logger.error("Cannot read Git repository: ${e.message}")
default
}
}
}
private val isIdeSync: Boolean
get() = java.lang.Boolean.parseBoolean(System.getProperty("idea.sync.active", "false"))
}

View File

@@ -22,19 +22,19 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin
abstract class DependencyCheck : DefaultTask() {
@get:Input
abstract val configuration: ListProperty<Configuration>
protected abstract val dependencies: ListProperty<DependencyResult>
/**
* A mapping of module coordinates (`group:module`) to versions, overriding the requested version.
*/
@get:Input
abstract val overrides: MapProperty<String, String>
protected abstract val overrides: MapProperty<String, String>
init {
description = "Check :core's dependencies are consistent with Minecraft's."
group = LifecycleBasePlugin.VERIFICATION_GROUP
configuration.finalizeValueOnRead()
dependencies.finalizeValueOnRead()
overrides.finalizeValueOnRead()
}
@@ -45,13 +45,19 @@ abstract class DependencyCheck : DefaultTask() {
overrides.putAll(project.provider { mutableMapOf(module.get().module.toString() to version) })
}
/**
* Add a configuration to check.
*/
fun configuration(configuration: Provider<Configuration>) {
// We can't store the Configuration in the cache, so store the resolved dependencies instead.
dependencies.addAll(configuration.map { it.incoming.resolutionResult.allDependencies })
}
@TaskAction
fun run() {
var ok = true
for (configuration in configuration.get()) {
configuration.incoming.resolutionResult.allDependencies {
if (!check(this@allDependencies)) ok = false
}
for (configuration in dependencies.get()) {
if (!check(configuration)) ok = false
}
if (!ok) {

View File

@@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import net.minecraftforge.gradle.common.util.RunConfig
import net.minecraftforge.gradle.common.util.runs.setRunConfigInternal
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.JavaExec
import org.gradle.jvm.toolchain.JavaToolchainService
import java.nio.file.Files
/**
* Set [JavaExec] task to run a given [RunConfig].
*/
fun JavaExec.setRunConfig(config: RunConfig) {
dependsOn("prepareRuns")
setRunConfigInternal(project, this, config)
doFirst("Create working directory") { Files.createDirectories(workingDir.toPath()) }
javaLauncher.set(
project.extensions.getByType(JavaToolchainService::class.java)
.launcherFor(project.extensions.getByType(JavaPluginExtension::class.java).toolchain),
)
}

View File

@@ -1,73 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.plugins.BasePluginExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.specs.Spec
/**
* A dependency in a POM file.
*/
data class MavenDependency(val groupId: String?, val artifactId: String?, val version: String?, val scope: String?)
/**
* A spec specifying which dependencies to include/exclude.
*/
class MavenDependencySpec {
private val excludeSpecs = mutableListOf<Spec<MavenDependency>>()
fun exclude(spec: Spec<MavenDependency>) {
excludeSpecs.add(spec)
}
fun exclude(dep: Dependency) {
exclude {
// We have to cheat a little for project dependencies, as the project name doesn't match the artifact group.
val name = when (dep) {
is ProjectDependency -> dep.dependencyProject.extensions.getByType(BasePluginExtension::class.java).archivesName.get()
else -> dep.name
}
(dep.group.isNullOrEmpty() || dep.group == it.groupId) &&
(name.isNullOrEmpty() || name == it.artifactId) &&
(dep.version.isNullOrEmpty() || dep.version == it.version)
}
}
fun exclude(dep: MinimalExternalModuleDependency) {
exclude {
dep.module.group == it.groupId && dep.module.name == it.artifactId
}
}
fun isIncluded(dep: MavenDependency) = !excludeSpecs.any { it.isSatisfiedBy(dep) }
}
/**
* Configure dependencies present in this publication's POM file.
*
* While this approach is very ugly, it's the easiest way to handle it!
*/
fun MavenPublication.mavenDependencies(action: MavenDependencySpec.() -> Unit) {
val spec = MavenDependencySpec()
action(spec)
pom.withXml {
val dependencies = XmlUtil.findChild(asNode(), "dependencies") ?: return@withXml
dependencies.children().map { it as groovy.util.Node }.forEach {
val dep = MavenDependency(
groupId = XmlUtil.findChild(it, "groupId")?.text(),
artifactId = XmlUtil.findChild(it, "artifactId")?.text(),
version = XmlUtil.findChild(it, "version")?.text(),
scope = XmlUtil.findChild(it, "scope")?.text(),
)
if (!spec.isIncluded(dep)) it.parent().remove(it)
}
}
}

View File

@@ -24,7 +24,6 @@ class MinecraftConfigurations private constructor(private val project: Project)
private val java = project.extensions.getByType(JavaPluginExtension::class.java)
private val sourceSets = java.sourceSets
private val configurations = project.configurations
private val objects = project.objects
private val main = sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
private val test = sourceSets[SourceSet.TEST_SOURCE_SET_NAME]
@@ -37,13 +36,7 @@ class MinecraftConfigurations private constructor(private val project: Project)
val client = sourceSets.maybeCreate("client")
// Ensure the client classpaths behave the same as the main ones.
configurations.named(client.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
}
configurations.named(client.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
}
consistentWithMain(client)
// Set up an API configuration for clients (to ensure it's consistent with the main source set).
val clientApi = configurations.maybeCreate(client.apiConfigurationName).apply {
@@ -85,6 +78,16 @@ class MinecraftConfigurations private constructor(private val project: Project)
setupBasic()
}
private fun consistentWithMain(sourceSet: SourceSet) {
configurations.named(sourceSet.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
}
configurations.named(sourceSet.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
}
}
private fun setupBasic() {
val client = sourceSets["client"]
@@ -96,13 +99,30 @@ class MinecraftConfigurations private constructor(private val project: Project)
val checkDependencyConsistency =
project.tasks.register("checkDependencyConsistency", DependencyCheck::class.java) {
// We need to check both the main and client classpath *configurations*, as the actual configuration
configuration.add(configurations.named(main.runtimeClasspathConfigurationName))
configuration.add(configurations.named(client.runtimeClasspathConfigurationName))
configuration(configurations.named(main.runtimeClasspathConfigurationName))
configuration(configurations.named(client.runtimeClasspathConfigurationName))
}
project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
}
/**
* Create a new configuration that pulls in the main and client classes from the mod.
*/
private fun createDerivedConfiguration(name: String) {
val client = sourceSets["client"]
val sourceSet = sourceSets.create(name)
sourceSet.compileClasspath += main.compileClasspath + client.compileClasspath
sourceSet.runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
consistentWithMain(sourceSet)
project.dependencies.add(sourceSet.implementationConfigurationName, main.output)
project.dependencies.add(sourceSet.implementationConfigurationName, client.output)
}
companion object {
const val DATAGEN = "datagen"
const val EXAMPLES = "examples"
const val TEST_MOD = "testMod"
fun setupBasic(project: Project) {
MinecraftConfigurations(project).setupBasic()
}
@@ -110,6 +130,10 @@ class MinecraftConfigurations private constructor(private val project: Project)
fun setup(project: Project) {
MinecraftConfigurations(project).setup()
}
fun createDerivedConfiguration(project: Project, name: String) {
MinecraftConfigurations(project).createDerivedConfiguration(name)
}
}
}

View File

@@ -4,7 +4,7 @@
package cc.tweaked.gradle
import net.minecraftforge.gradle.common.util.RunConfig
import net.neoforged.moddevgradle.internal.RunGameTask
import org.gradle.api.GradleException
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.invocation.Gradle
@@ -65,11 +65,19 @@ abstract class ClientJavaExec : JavaExec() {
setTestProperties()
}
fun copyFromForge(path: String) = copyFromForge(project.tasks.getByName(path, RunGameTask::class))
/**
* Set this task to run a given [RunConfig].
* Set this task to run a given [RunGameTask].
*/
fun setRunConfig(config: RunConfig) {
(this as JavaExec).setRunConfig(config)
fun copyFromForge(task: RunGameTask) {
copyFrom(task)
// Eagerly evaluate the behaviour of RunGameTask.exec
environment.putAll(task.environmentProperty.get())
classpath(task.classpathProvider)
workingDir = task.gameDirectory.get().asFile
setTestProperties() // setRunConfig may clobber some properties, ensure everything is set.
}

View File

@@ -11,7 +11,9 @@ import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import java.io.File
import javax.inject.Inject
class NodePlugin : Plugin<Project> {
override fun apply(project: Project) {
@@ -43,9 +45,12 @@ abstract class NpmInstall : DefaultTask() {
@get:OutputDirectory
val nodeModules: Provider<Directory> = projectRoot.dir("node_modules")
@get:Inject
protected abstract val execOperations: ExecOperations
@TaskAction
fun install() {
project.exec {
execOperations.exec {
commandLine(ProcessHelpers.getExecutable("npm"), "ci")
workingDir = projectRoot.get().asFile
}

View File

@@ -4,45 +4,10 @@
package cc.tweaked.gradle
import org.codehaus.groovy.runtime.ProcessGroovyMethods
import org.gradle.api.GradleException
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
internal object ProcessHelpers {
fun startProcess(vararg command: String): Process {
// Something randomly passes in "GIT_DIR=" as an environment variable which clobbers everything else. Don't
// inherit the environment array!
return ProcessBuilder()
.command(*command)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.also { it.environment().clear() }
.start()
}
fun captureOut(vararg command: String): String {
val process = startProcess(*command)
process.outputStream.close()
val result = ProcessGroovyMethods.getText(process)
process.waitForOrThrow("Failed to run command")
return result
}
fun captureLines(vararg command: String): List<String> {
val process = startProcess(*command)
process.outputStream.close()
val out = BufferedReader(InputStreamReader(process.inputStream, StandardCharsets.UTF_8)).use { reader ->
reader.lines().filter { it.isNotEmpty() }.toList()
}
ProcessGroovyMethods.closeStreams(process)
process.waitForOrThrow("Failed to run command")
return out
}
fun onPath(name: String): Boolean {
val path = System.getenv("PATH") ?: return false
return path.splitToSequence(File.pathSeparator).any { File(it, name).exists() }

View File

@@ -1,51 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package net.minecraftforge.gradle.common.util.runs
import net.minecraftforge.gradle.common.util.RunConfig
import org.gradle.api.Project
import org.gradle.process.CommandLineArgumentProvider
import org.gradle.process.JavaExecSpec
import java.io.File
import java.util.function.Supplier
import java.util.stream.Collectors
import java.util.stream.Stream
/**
* Set up a [JavaExecSpec] to execute a [RunConfig].
*
* [MinecraftRunTask] sets up all its properties when the task is executed, rather than when configured. As such, it's
* not possible to use [cc.tweaked.gradle.copyToFull] like we do for Fabric. Instead, we set up the task manually.
*
* Unfortunately most of the functionality we need is package-private, and so we have to put our code into the package.
*/
internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: RunConfig) {
spec.workingDir = File(config.workingDirectory)
spec.mainClass.set(config.main)
for (source in config.allSources) spec.classpath(source.runtimeClasspath)
val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java)
// Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts.
val lazyTokens = RunConfigGenerator.configureTokensLazy(
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
originalTask.get().minecraftArtifacts,
originalTask.get().runtimeClasspathArtifacts,
)
spec.argumentProviders.add(
CommandLineArgumentProvider {
RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()
},
)
spec.jvmArgumentProviders.add(
CommandLineArgumentProvider {
(if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } +
config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" }
},
)
for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value))
}

View File

@@ -124,7 +124,7 @@ SPDX-License-Identifier: MPL-2.0
</module>
<module name="MethodTypeParameterName" />
<module name="PackageName">
<property name="format" value="^(dan200\.computercraft|cc\.tweaked)(\.[a-z][a-z0-9]*)*" />
<property name="format" value="^(dan200\.computercraft|cc\.tweaked|com\.example\.examplemod)(\.[a-z][a-z0-9]*)*" />
</module>
<module name="ParameterName" />
<module name="StaticVariableName">

View File

@@ -8,7 +8,7 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0
-->
The [`monitor_resize`] event is fired when an adjacent or networked monitor's size is changed.
The [`monitor_resize`] event is fired when an adjacent or networked [monitor's][`monitor`] size is changed.
## Return Values
1. [`string`]: The event name.

View File

@@ -8,7 +8,7 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0
-->
The [`monitor_touch`] event is fired when an adjacent or networked Advanced Monitor is right-clicked.
The [`monitor_touch`] event is fired when an adjacent or networked [Advanced Monitor][`monitor`] is right-clicked.
## Return Values
1. [`string`]: The event name.

View File

@@ -16,7 +16,7 @@ CC: Tweaked is a mod for Minecraft which adds programmable computers, turtles an
much-beloved [ComputerCraft], it continues its legacy with improved performance and stability, along with a wealth of
new features.
CC: Tweaked can be installed from [CurseForge] or [Modrinth]. It runs on both [Minecraft Forge] and [Fabric].
CC: Tweaked can be installed from [Modrinth]. It runs on both [Minecraft Forge] and [Fabric].
## Features
Controlled using the [Lua programming language][lua], CC: Tweaked's computers provides all the tools you need to start
@@ -62,7 +62,6 @@ CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please
[github]: https://github.com/cc-tweaked/CC-Tweaked/ "CC: Tweaked on GitHub"
[bug]: https://github.com/cc-tweaked/CC-Tweaked/issues/new/choose
[computercraft]: https://github.com/dan200/ComputerCraft "ComputerCraft on GitHub"
[curseforge]: https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked from CurseForge"
[modrinth]: https://modrinth.com/mod/gu7yAYhd "Download CC: Tweaked from Modrinth"
[forge]: https://files.minecraftforge.net/ "Download Minecraft Forge."
[Minecraft Forge]: https://files.minecraftforge.net/ "Download Minecraft Forge."

View File

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

View File

@@ -31,9 +31,9 @@ commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2"
jzlib = "1.1.3"
kotlin = "1.9.21"
kotlin-coroutines = "1.7.3"
nightConfig = "3.6.7"
kotlin = "2.1.0"
kotlin-coroutines = "1.10.1"
nightConfig = "3.8.1"
# Minecraft mods
emi = "1.0.8+1.20.1"
@@ -52,30 +52,29 @@ create-fabric = "0.5.1-f-build.1467+mc1.20.1"
# Testing
hamcrest = "2.2"
jqwik = "1.8.2"
junit = "5.10.1"
junit = "5.11.4"
junitPlatform = "1.11.4"
jmh = "1.37"
# Build tools
cctJavadoc = "1.8.3"
checkstyle = "10.14.1"
curseForgeGradle = "1.0.14"
errorProne-core = "2.27.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.7.1"
forgeGradle = "6.0.21"
fabric-loom = "1.9.2"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
illuaminate = "0.1.0-74-gf1551d5"
librarian = "1.+"
lwjgl = "3.3.3"
minotaur = "2.+"
minotaur = "2.8.7"
modDevGradle = "2.0.74"
nullAway = "0.10.25"
shadow = "8.3.1"
spotless = "6.23.3"
taskTree = "2.1.1"
teavm = "0.11.0-SQUID.1"
vanillaExtract = "0.1.3"
vanillaExtract = "0.2.0"
versionCatalogUpdate = "0.8.1"
[libraries]
@@ -132,6 +131,7 @@ jqwik-engine = { module = "net.jqwik:jqwik-engine", version.ref = "jqwik" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatform" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
@@ -145,19 +145,17 @@ lwjgl-glfw = { module = "org.lwjgl:lwjgl-glfw" }
# Build tools
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
curseForgeGradle = { module = "net.darkhax.curseforgegradle:CurseForgeGradle", version.ref = "curseForgeGradle" }
errorProne-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorProne-core" }
errorProne-api = { module = "com.google.errorprone:error_prone_check_api", version.ref = "errorProne-core" }
errorProne-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorProne-core" }
errorProne-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorProne-plugin" }
errorProne-testHelpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "errorProne-core" }
fabric-loom = { module = "net.fabricmc:fabric-loom", version.ref = "fabric-loom" }
forgeGradle = { module = "net.minecraftforge.gradle:ForgeGradle", version.ref = "forgeGradle" }
ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "ideaExt" }
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" }
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" }
modDevGradle = { module = "net.neoforged:moddev-gradle", version.ref = "modDevGradle" }
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" }
teavm-core = { module = "org.teavm:teavm-core", version.ref = "teavm" }
@@ -172,11 +170,9 @@ vanillaExtract = { module = "cc.tweaked.vanilla-extract:plugin", version.ref = "
yarn = { module = "net.fabricmc:yarn", version.ref = "yarn" }
[plugins]
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" }
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }
@@ -194,7 +190,7 @@ externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
# Testing
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
testRuntime = ["junit-jupiter-engine", "jqwik-engine"]
testRuntime = ["junit-jupiter-engine", "junit-platform-launcher", "jqwik-engine"]
# Build tools
teavm-api = ["teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api"]

View File

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

3
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

View File

@@ -18,13 +18,28 @@ dependencies {
api(project(":core-api"))
}
val javadocOverview by tasks.registering(Copy::class) {
from("src/overview.html")
into(layout.buildDirectory.dir(name))
expand(
mapOf(
"mcVersion" to mcVersion,
"modVersion" to version,
),
)
}
tasks.javadoc {
title = "CC: Tweaked $version Minecraft $mcVersion"
title = "CC: Tweaked $version for Minecraft $mcVersion"
include("dan200/computercraft/api/**/*.java")
options {
(this as StandardJavadocDocletOptions)
inputs.files(javadocOverview)
overview(javadocOverview.get().destinationDir.resolve("overview.html").absolutePath)
groups = mapOf(
"Common" to listOf(
"dan200.computercraft.api",
@@ -47,6 +62,15 @@ tasks.javadoc {
<link href=" https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css " rel="stylesheet">
""".trimIndent(),
)
taglets("cc.tweaked.javadoc.SnippetTaglet")
tagletPath(configurations.detachedConfiguration(dependencies.project(":lints")).toList())
val snippetSources = listOf(":common", ":fabric", ":forge").flatMap {
project(it).sourceSets["examples"].allSource.sourceDirectories
}
inputs.files(snippetSources)
jFlags("-Dcc.snippet-path=" + snippetSources.joinToString(File.pathSeparator) { it.absolutePath })
}
// Include the core-api in our javadoc export. This is wrong, but it means we can export a single javadoc dump.

View File

@@ -22,7 +22,14 @@ import java.util.List;
* <p>
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
* on Forge
* on Forge.
*
* <h2>Example</h2>
* <h3>Fabric</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h3>Forge</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* @param <T> The type of turtle upgrade this modeller applies to.
* @see RegisterTurtleUpgradeModeller For multi-loader registration support.

View File

@@ -171,16 +171,9 @@ public final class ComputerCraftAPI {
* 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:
* computers. For example, one can add a new API just to turtles with the following code:
*
* <pre class="language language-java">{@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>
* {@snippet class=com.example.examplemod.ExampleAPI region=register}
*
* @param factory The factory for your API subclass.
* @see ILuaAPIFactory

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.media;
import dan200.computercraft.impl.ComputerCraftAPIService;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.stream.Stream;
/**
* The contents of a page (or book) created by a ComputerCraft printer.
*
* @since 1.115
*/
@Nullable
public interface PrintoutContents {
/**
* Get the (possibly empty) title for this printout.
*
* @return The title of this printout.
*/
String getTitle();
/**
* Get the text contents of this printout, as a sequence of lines.
* <p>
* The lines in the printout may include blank lines at the end of the document, as well as trailing spaces on each
* line.
*
* @return The text contents of this printout.
*/
Stream<String> getTextLines();
/**
* Get the printout contents for a particular stack.
*
* @param stack The stack to get the contents for.
* @return The printout contents, or {@code null} if this is not a printout item.
*/
static @Nullable PrintoutContents get(ItemStack stack) {
return ComputerCraftAPIService.get().getPrintoutContents(stack);
}
}

View File

@@ -19,8 +19,8 @@ import dan200.computercraft.api.ComputerCraftAPI;
*/
public interface WiredElement extends WiredSender {
/**
* Called when objects on the network change. This may occur when network nodes are added or removed, or when
* peripherals change.
* Called when peripherals on the network change. This may occur when network nodes are added or removed, or when
* peripherals are attached or detached from a modem.
*
* @param change The change which occurred.
* @see WiredNetworkChange

View File

@@ -14,13 +14,10 @@ import javax.annotation.Nullable;
* A peripheral which can be equipped to the back side of a pocket computer.
* <p>
* Pocket upgrades are defined in two stages. First, on creates a {@link IPocketUpgrade} subclass and corresponding
* {@link PocketUpgradeSerialiser} instance, which are then registered in a Forge registry.
* {@link PocketUpgradeSerialiser} instance, which are then registered in a Minecraft registry.
* <p>
* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and
* the upgrade registered internally. See the documentation in {@link PocketUpgradeSerialiser} for details on this process
* and where files should be located.
*
* @see PocketUpgradeSerialiser For how to register a pocket computer upgrade.
* the upgrade registered internally.
*/
public interface IPocketUpgrade extends UpgradeBase {
/**

View File

@@ -8,21 +8,69 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.upgrades.UpgradeBase;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.Items;
import javax.annotation.Nullable;
import java.util.function.BiFunction;
/**
* The primary interface for defining an update for Turtles. A turtle update can either be a new tool, or a new
* peripheral.
* <p>
* Turtle upgrades are defined in two stages. First, one creates a {@link ITurtleUpgrade} subclass and corresponding
* {@link TurtleUpgradeSerialiser} instance, which are then registered in a Forge registry.
* {@link TurtleUpgradeSerialiser} instance, which are then registered in a Minecraft registry.
* <p>
* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and
* the upgrade registered internally. See the documentation in {@link TurtleUpgradeSerialiser} for details on this process
* and where files should be located.
* the upgrade automatically registered.
*
* @see TurtleUpgradeSerialiser For how to register a turtle upgrade.
* <h2>Example</h2>
* <h3>Registering the upgrade serialiser</h3>
* First, let's create a new class that implements {@link ITurtleUpgrade}. It is recommended to subclass
* {@link AbstractTurtleUpgrade}, as that provides a default implementation of most methods.
* <p>
* {@snippet class=com.example.examplemod.ExampleTurtleUpgrade region=body}
* <p>
* Now we must construct a new upgrade serialiser. In most cases, you can use one of the helper methods
* (e.g. {@link TurtleUpgradeSerialiser#simpleWithCustomItem(BiFunction)}), rather than defining your own implementation.
*
* {@snippet class=com.example.examplemod.ExampleMod region=turtle_upgrades}
*
* We now must register this upgrade serialiser. This is done the same way as you'd register blocks, items, or other
* Minecraft objects. The approach to do this will depend on mod-loader.
*
* <h4>Fabric</h4>
* {@snippet class=com.example.examplemod.FabricExampleMod region=turtle_upgrades}
*
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.ForgeExampleMod region=turtle_upgrades}
*
* <h3>Rendering the upgrade</h3>
* Next, we need to register a model for our upgrade. This is done by registering a
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for your upgrade serialiser.
*
* <h4>Fabric</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
*
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h3>Registering the upgrade itself</h3>
* Upgrades themselves are loaded from datapacks when a level is loaded. In order to register our new upgrade, we must
* create a new JSON file at {@code data/<my_mod>/computercraft/turtle_upgrades/<my_upgrade_id>.json}.
*
* {@snippet file = data/examplemod/computercraft/turtle_upgrades/example_turtle_upgrade.json}
*
* The {@code "type"} field points to the ID of the upgrade serialiser we've just registered, while the other fields
* are read by the serialiser itself. As our upgrade was defined with {@link TurtleUpgradeSerialiser#simpleWithCustomItem(BiFunction)}, the
* {@code "item"} field will construct our upgrade with {@link Items#COMPASS}.
* <p>
* Rather than manually creating the file, it is recommended to data-generators to generate this file. This can be done
* with {@link TurtleUpgradeDataProvider}.
*
* {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body}
*
* @see TurtleUpgradeSerialiser Registering a turtle upgrade.
*/
public interface ITurtleUpgrade extends UpgradeBase {
/**

View File

@@ -29,6 +29,9 @@ import java.util.function.Consumer;
* {@link #addUpgrades(Consumer)} function, construct each upgrade, and pass them off to the provided consumer to
* generate them.
*
* <h2>Example</h2>
* {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body}
*
* @see TurtleUpgradeSerialiser
*/
public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITurtleUpgrade, TurtleUpgradeSerialiser<?>> {

View File

@@ -27,32 +27,6 @@ import java.util.function.Function;
* If your turtle upgrade doesn't have any associated configurable parameters (like most upgrades), you can use
* {@link #simple(Function)} or {@link #simpleWithCustomItem(BiFunction)} to create a basic upgrade serialiser.
*
* <h2>Example (Forge)</h2>
* <pre class="language language-java">{@code
* static final DeferredRegister<TurtleUpgradeSerialiser<?>> SERIALISERS = DeferredRegister.create( TurtleUpgradeSerialiser.TYPE, "my_mod" );
*
* // Register a new upgrade serialiser called "my_upgrade".
* public static final RegistryObject<TurtleUpgradeSerialiser<MyUpgrade>> MY_UPGRADE =
* SERIALISERS.register( "my_upgrade", () -> TurtleUpgradeSerialiser.simple( MyUpgrade::new ) );
*
* // Then in your constructor
* SERIALISERS.register( bus );
* }</pre>
* <p>
* We can then define a new upgrade using JSON by placing the following in
* {@literal data/<my_mod>/computercraft/turtle_upgrades/<my_upgrade_id>.json}}.
*
* <pre class="language language-json">{@code
* {
* "type": "my_mod:my_upgrade",
* }
* }</pre>
* <p>
* Finally, we need to register a model for our upgrade. The way to do this varies on mod loader, see
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information.
* <p>
* {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
*
* @param <T> The type of turtle upgrade this is responsible for serialising.
* @see ITurtleUpgrade
* @see TurtleUpgradeDataProvider

View File

@@ -32,6 +32,8 @@ import java.util.function.Function;
*
* @param <T> The base class of upgrades.
* @param <R> The upgrade serialiser to register for.
* @see dan200.computercraft.api.turtle.TurtleUpgradeDataProvider
* @see dan200.computercraft.api.pocket.PocketUpgradeDataProvider
*/
public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends UpgradeSerialiser<? extends T>> implements DataProvider {
private final PackOutput output;
@@ -84,13 +86,9 @@ public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends Upgra
/**
* Add all turtle or pocket computer upgrades.
* <p>
* <strong>Example usage:</strong>
* <pre class="language language-java">{@code
* protected void addUpgrades(Consumer<Upgrade<TurtleUpgradeSerialiser<?>>> addUpgrade) {
* simple(new ResourceLocation("mymod", "speaker"), SPEAKER_SERIALISER.get()).add(addUpgrade);
* }
* }</pre>
*
* <h4>Example</h4>
* {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body}
*
* @param addUpgrade A callback used to register an upgrade.
*/

View File

@@ -12,6 +12,7 @@ import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.api.media.PrintoutContents;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
@@ -75,6 +76,9 @@ public interface ComputerCraftAPIService {
DetailRegistry<BlockReference> getBlockInWorldDetailRegistry();
@Nullable
PrintoutContents getPrintoutContents(ItemStack stack);
final class Instance {
static final @Nullable ComputerCraftAPIService INSTANCE;
static final @Nullable Throwable ERROR;

View File

@@ -0,0 +1,68 @@
<!--
SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0
-->
<!DOCTYPE HTML>
<html lang="en">
<body>
<p>
This is the documentation for CC: Tweaked $modVersion for Minecraft $mcVersion. Documentation for other versions of
Minecraft are available on the CC: Tweaked website:
<ul>
<li><a href="/mc-1.20.x/javadoc/">Minecraft 1.20.1</a>
<li><a href="/mc-1.21.x/javadoc/">Minecraft 1.21.1</a>
</ul>
<h1>Quick links</h1>
<p>
You probably want to start in the following places:
<ul>
<li>{@linkplain dan200.computercraft.api.peripheral Registering new peripherals}</li>
<li>
{@link dan200.computercraft.api.lua.LuaFunction} and {@link dan200.computercraft.api.lua.IArguments} for
adding methods to your peripheral or Lua objects.
</li>
<li>{@linkplain dan200.computercraft.api.turtle.ITurtleUpgrade Turtle upgrades}</li>
<li>{@linkplain dan200.computercraft.api.pocket.IPocketUpgrade Pocket upgrades}</li>
</ul>
<h1>Using</h1>
<p>
CC: Tweaked is hosted on my maven repo, and so is relatively simple to depend on. You may wish to add a soft (or
hard) dependency in your <code>mods.toml</code> file, with the appropriate version bounds, to ensure that API
functionality you depend on is present.
<pre class="language language-groovy"><code>repositories {
maven {
url "https://maven.squiddev.cc"
content { includeGroup("cc.tweaked") }
}
}
dependencies {
// Vanilla (i.e. for multi-loader systems)
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api:$modVersion")
// Forge Gradle
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-core-api:$modVersion")
compileOnly(fg.deobf("cc.tweaked:cc-tweaked-$mcVersion-forge-api:$modVersion"))
runtimeOnly(fg.deobf("cc.tweaked:cc-tweaked-$mcVersion-forge:$modVersion"))
// Fabric Loom
modCompileOnly("cc.tweaked:cc-tweaked-$mcVersion-fabric-api:$modVersion")
modRuntimeOnly("cc.tweaked:cc-tweaked-$mcVersion-fabric:$modVersion")
}
</code></pre>
<p>
You should also be careful to only use classes within the <code>dan200.computercraft.api</code> package. Non-API
classes are subject to change at any point. If you depend on functionality outside the API (or need to mixin to
CC:T), please <a href="https://github.com/cc-tweaked/CC-Tweaked/discussions/new/choose">start a discussion</a> to
let me know!
</body>
</html>

View File

@@ -6,17 +6,11 @@ import cc.tweaked.gradle.*
plugins {
id("cc-tweaked.vanilla")
id("cc-tweaked.gametest")
id("cc-tweaked.illuaminate")
id("cc-tweaked.mod")
id("cc-tweaked.publishing")
}
sourceSets {
main {
resources.srcDir("src/generated/resources")
}
}
minecraft {
accessWideners(
"src/main/resources/computercraft.accesswidener",
@@ -67,7 +61,7 @@ dependencies {
}
illuaminate {
version.set(libs.versions.illuaminate)
version = libs.versions.illuaminate
}
val luaJavadoc by tasks.registering(Javadoc::class) {
@@ -88,11 +82,7 @@ val luaJavadoc by tasks.registering(Javadoc::class) {
options.addStringOption("project-root", rootProject.file(".").absolutePath)
options.noTimestamp(false)
javadocTool.set(
javaToolchains.javadocToolFor {
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
},
)
javadocTool = javaToolchains.javadocToolFor { languageVersion = CCTweakedPlugin.JAVA_VERSION }
}
val lintLua by tasks.registering(IlluaminateExec::class) {
@@ -113,20 +103,31 @@ val lintLua by tasks.registering(IlluaminateExec::class) {
doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
}
val runData by tasks.registering(MergeTrees::class) {
output = layout.projectDirectory.dir("src/generated/resources")
fun MergeTrees.configureForDatagen(source: SourceSet, outputFolder: String) {
output = layout.projectDirectory.dir(outputFolder)
for (loader in listOf("forge", "fabric")) {
mustRunAfter(":$loader:runData")
mustRunAfter(":$loader:$name")
source {
input {
from(project(":$loader").layout.buildDirectory.dir("generatedResources"))
from(project(":$loader").layout.buildDirectory.dir(source.getTaskName("generateResources", null)))
exclude(".cache")
}
output = project(":$loader").layout.projectDirectory.dir("src/generated/resources")
output = project(":$loader").layout.projectDirectory.dir(outputFolder)
}
}
}
tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false }
val runData by tasks.registering(MergeTrees::class) {
configureForDatagen(sourceSets.main.get(), "src/generated/resources")
}
val runExampleData by tasks.registering(MergeTrees::class) {
configureForDatagen(sourceSets.examples.get(), "src/examples/generatedResources")
}
// We can't create accurate module metadata for our additional capabilities, so disable it.
project.tasks.withType(GenerateModuleMetadata::class.java).configureEach {
isEnabled = false
}

View File

@@ -10,10 +10,10 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
import dan200.computercraft.shared.network.server.KeyEventServerMessage;
import dan200.computercraft.shared.network.server.MouseEventServerMessage;
import dan200.computercraft.shared.network.server.QueueEventServerMessage;
import dan200.computercraft.shared.network.server.PasteEventComputerMessage;
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* An {@link InputHandler} for use on the client.
@@ -27,6 +27,11 @@ public final class ClientInputHandler implements InputHandler {
this.menu = menu;
}
@Override
public void terminate() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE));
}
@Override
public void turnOn() {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
@@ -42,11 +47,6 @@ public final class ClientInputHandler implements InputHandler {
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
}
@Override
public void keyDown(int key, boolean repeat) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key));
@@ -57,6 +57,16 @@ public final class ClientInputHandler implements InputHandler {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.UP, key));
}
@Override
public void charTyped(byte chr) {
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.CHAR, chr));
}
@Override
public void paste(ByteBuffer contents) {
ClientNetworking.sendToServer(new PasteEventComputerMessage(menu, contents));
}
@Override
public void mouseClick(int button, int x, int y) {
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.Action.CLICK, button, x, y));

View File

@@ -6,7 +6,6 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.data.client.ClientDataProviders;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureManager;
@@ -33,10 +32,13 @@ public final class GuiSprites extends TextureAtlasHolder {
public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true);
public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false);
public static final ResourceLocation TURTLE_NORMAL_SELECTED_SLOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/turtle_normal_selected_slot");
public static final ResourceLocation TURTLE_ADVANCED_SELECTED_SLOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/turtle_advanced_selected_slot");
private static ButtonTextures button(String name) {
return new ButtonTextures(
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/buttons/" + name),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/buttons/" + name + "_hover")
);
}
@@ -113,7 +115,6 @@ public final class GuiSprites extends TextureAtlasHolder {
* @param pocketBottom The texture for the bottom of a pocket computer.
* @param sidebar The texture for the computer sidebar.
* @see ComputerBorderRenderer
* @see ClientDataProviders
*/
public record ComputerTextures(
ResourceLocation border,

View File

@@ -54,9 +54,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
if (slot >= 0) {
var slotX = slot % 4;
var slotY = slot / 4;
graphics.blit(texture,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0,
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE
graphics.blit(
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22,
GuiSprites.get(advanced ? GuiSprites.TURTLE_ADVANCED_SELECTED_SLOT : GuiSprites.TURTLE_NORMAL_SELECTED_SLOT)
);
}

View File

@@ -55,7 +55,7 @@ public final class ComputerSidebar {
add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT,
GuiSprites.TERMINATE::get,
b -> input.queueEvent("terminate"),
b -> input.terminate(),
new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key")

View File

@@ -71,11 +71,8 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean charTyped(char ch, int modifiers) {
if (ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255) {
// Queue the char event for any printable chars in byte range
computer.queueEvent("char", new Object[]{ Character.toString(ch) });
}
var terminalChar = StringUtil.unicodeToTerminal(ch);
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
return true;
}
@@ -112,8 +109,8 @@ public class TerminalWidget extends AbstractWidget {
}
private void paste() {
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
var clipboard = StringUtil.getClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (clipboard.remaining() > 0) computer.paste(clipboard);
}
@Override
@@ -222,7 +219,7 @@ public class TerminalWidget extends AbstractWidget {
public void update() {
if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) {
computer.queueEvent("terminate");
computer.terminate();
}
if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) {

View File

@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.pocket.PocketComputerData;
import dan200.computercraft.client.render.CustomLecternRenderer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FastColor;
import net.minecraft.world.inventory.InventoryMenu;
/**
* A model for {@linkplain PocketComputerItem pocket computers} placed on a lectern.
*
* @see CustomLecternRenderer
*/
public class LecternPocketModel {
public static final ResourceLocation TEXTURE_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_normal");
public static final ResourceLocation TEXTURE_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_advanced");
public static final ResourceLocation TEXTURE_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_colour");
public static final ResourceLocation TEXTURE_FRAME = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
public static final ResourceLocation TEXTURE_LIGHT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_light");
private static final Material MATERIAL_NORMAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_NORMAL);
private static final Material MATERIAL_ADVANCED = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_ADVANCED);
private static final Material MATERIAL_COLOUR = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_COLOUR);
private static final Material MATERIAL_FRAME = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_FRAME);
private static final Material MATERIAL_LIGHT = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_LIGHT);
// The size of the terminal within the model.
public static final float TERM_WIDTH = 12.0f / 32.0f;
public static final float TERM_HEIGHT = 14.0f / 32.0f;
// The size of the texture. The texture is 36x36, but is at 2x resolution.
private static final int TEXTURE_WIDTH = 36 / 2;
private static final int TEXTURE_HEIGHT = 36 / 2;
private final ModelPart root;
public LecternPocketModel() {
root = buildPages();
}
private static ModelPart buildPages() {
var mesh = new MeshDefinition();
var parts = mesh.getRoot();
parts.addOrReplaceChild(
"root",
CubeListBuilder.create().texOffs(0, 0).addBox(0f, -5.0f, -4.0f, 1f, 10.0f, 8.0f),
PartPose.ZERO
);
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
}
private void render(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay, int colour) {
int red = FastColor.ARGB32.red(colour), green = FastColor.ARGB32.green(colour), blue = FastColor.ARGB32.blue(colour), alpha = FastColor.ARGB32.alpha(colour);
root.render(poseStack, buffer, packedLight, packedOverlay, red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f);
}
/**
* Render the pocket computer model.
*
* @param poseStack The current pose stack.
* @param bufferSource The buffer source to draw to.
* @param packedLight The current light level.
* @param packedOverlay The overlay texture (used for entity hurt animation).
* @param family The computer family.
* @param frameColour The pocket computer's {@linkplain PocketComputerItem#getColour(ItemStack) colour}.
* @param lightColour The pocket computer's {@linkplain PocketComputerData#getLightState() light colour}.
*/
public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay, ComputerFamily family, int frameColour, int lightColour) {
if (frameColour != -1) {
root.render(poseStack, MATERIAL_FRAME.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay, 1, 1, 1, 1);
render(poseStack, MATERIAL_COLOUR.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay, frameColour);
} else {
var buffer = (family == ComputerFamily.ADVANCED ? MATERIAL_ADVANCED : MATERIAL_NORMAL).buffer(bufferSource, RenderType::entityCutout);
root.render(poseStack, buffer, packedLight, packedOverlay, 1, 1, 1, 1);
}
render(poseStack, MATERIAL_LIGHT.buffer(bufferSource, RenderType::entityCutout), LightTexture.FULL_BRIGHT, packedOverlay, lightColour);
}
}

View File

@@ -6,15 +6,28 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.util.ARGB32;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.LecternRenderer;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.phys.Vec3;
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
/**
* A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}.
@@ -22,10 +35,17 @@ import net.minecraft.world.level.block.LecternBlock;
* This largely follows {@link LecternRenderer}, but with support for multiple types of item.
*/
public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternBlockEntity> {
private static final int POCKET_TERMINAL_RENDER_DISTANCE = 32;
private final BlockEntityRenderDispatcher berDispatcher;
private final LecternPrintoutModel printoutModel;
private final LecternPocketModel pocketModel;
public CustomLecternRenderer(BlockEntityRendererProvider.Context context) {
berDispatcher = context.getBlockEntityRenderDispatcher();
printoutModel = new LecternPrintoutModel();
pocketModel = new LecternPocketModel();
}
@Override
@@ -44,8 +64,46 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
} else {
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutItem.getPageCount(item));
}
} else if (item.getItem() instanceof PocketComputerItem pocket) {
var computer = ClientPocketComputers.get(item);
pocketModel.render(
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), pocket.getColour(item),
ARGB32.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
);
// Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
poseStack.mulPose(Axis.YP.rotationDegrees(90f));
poseStack.translate(-0.5 * LecternPocketModel.TERM_WIDTH, 0.5 * LecternPocketModel.TERM_HEIGHT + 1f / 32.0f, 1 / 16.0f);
poseStack.mulPose(Axis.XP.rotationDegrees(180));
// Either render the terminal or a black screen, depending on how close we are.
var terminal = computer == null ? null : computer.getTerminal();
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(RenderTypes.TERMINAL));
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
renderPocketTerminal(poseStack, quadEmitter, terminal);
} else {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);
}
}
poseStack.popPose();
}
private static void renderPocketTerminal(PoseStack poseStack, FixedWidthFontRenderer.QuadEmitter quadEmitter, Terminal terminal) {
var width = terminal.getWidth() * FONT_WIDTH;
var height = terminal.getHeight() * FONT_HEIGHT;
// Scale the terminal down to fit in the available space.
var scaleX = LecternPocketModel.TERM_WIDTH / (width + MARGIN * 2);
var scaleY = LecternPocketModel.TERM_HEIGHT / (height + MARGIN * 2);
var scale = Math.min(scaleX, scaleY);
poseStack.scale(scale, scale, -1.0f);
// Convert the model dimensions to terminal space, then find out how large the margin should be.
var marginX = ((LecternPocketModel.TERM_WIDTH / scale) - width) / 2;
var marginY = ((LecternPocketModel.TERM_HEIGHT / scale) - height) / 2;
FixedWidthFontRenderer.drawTerminal(quadEmitter, marginX, marginY, terminal, marginY, marginY, marginX, marginX);
}
}

View File

@@ -38,8 +38,8 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
int termWidth, termHeight;
if (terminal == null) {
termWidth = Config.pocketTermWidth;
termHeight = Config.pocketTermHeight;
termWidth = Config.DEFAULT_POCKET_TERM_WIDTH;
termHeight = Config.DEFAULT_POCKET_TERM_HEIGHT;
} else {
termWidth = terminal.getWidth();
termHeight = terminal.getHeight();

View File

@@ -176,7 +176,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
// In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
// and starting and ending offset, and so need to use two buffers instead.
// a starting and ending offset, and so need to use two buffers instead.
renderToBuffer(backgroundBuffer, size, sink ->
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
@@ -208,10 +208,10 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
foregroundBuffer.bind();
foregroundBuffer.drawWithShader(
matrix, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader(),
// As mentioned in the above comment, render the extra cursor quad if it is visible this frame. Each
// // quad has an index count of 6.
FixedWidthFontRenderer.isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()
? foregroundBuffer.getIndexCount() + 6 : foregroundBuffer.getIndexCount()
// Skip the cursor quad if it is not visible this frame.
FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
? foregroundBuffer.getIndexCount() - RenderTypes.TERMINAL.mode().indexCount(4)
: foregroundBuffer.getIndexCount()
);
// Clear state

View File

@@ -43,7 +43,7 @@ public final class FixedWidthFontRenderer {
static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
private static final float Z_OFFSET = 1e-3f;
private static final float Z_OFFSET = 1e-4f;
private FixedWidthFontRenderer() {
}

View File

@@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data.client;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.data.DataProviders;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
/**
* A version of {@link DataProviders} which relies on client-side classes.
* <p>
* This is called from {@link DataProviders#add(DataProviders.GeneratorSink)}.
*/
public final class ClientDataProviders {
private ClientDataProviders() {
}
public static void add(DataProviders.GeneratorSink generator) {
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(new ResourceLocation("blocks"), List.of(
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()),
new SingleFile(LecternPrintoutModel.TEXTURE, Optional.empty())
));
out.accept(GuiSprites.SPRITE_SHEET, Stream.of(
// Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
GuiSprites.COMPUTER_COMMAND.textures(),
GuiSprites.COMPUTER_COLOUR.textures()
).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList());
});
}
}

View File

@@ -5,6 +5,13 @@
package dan200.computercraft.data;
import com.mojang.serialization.Codec;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.data.tags.TagsProvider;
@@ -13,9 +20,14 @@ import net.minecraft.server.packs.PackType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* All data providers for ComputerCraft. We require a mod-loader abstraction {@link GeneratorSink} (instead of
@@ -39,14 +51,33 @@ public final class DataProviders {
generator.add(out -> new LanguageProvider(out, turtleUpgrades, pocketUpgrades));
// Unfortunately we rely on some client-side classes in this code. We just load in the client side data provider
// and invoke that.
try {
Class.forName("dan200.computercraft.data.client.ClientDataProviders")
.getMethod("add", GeneratorSink.class).invoke(null, generator);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(new ResourceLocation("blocks"), makeSprites(Stream.of(
UpgradeSlot.LEFT_UPGRADE,
UpgradeSlot.RIGHT_UPGRADE,
LecternPrintoutModel.TEXTURE,
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
)));
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
Stream.of(GuiSprites.TURTLE_NORMAL_SELECTED_SLOT, GuiSprites.TURTLE_ADVANCED_SELECTED_SLOT),
// Buttons
GuiSprites.TURNED_OFF.textures(),
GuiSprites.TURNED_ON.textures(),
GuiSprites.TERMINATE.textures(),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
GuiSprites.COMPUTER_COMMAND.textures(),
GuiSprites.COMPUTER_COLOUR.textures()
));
});
}
@SafeVarargs
@SuppressWarnings("varargs")
private static List<SpriteSource> makeSprites(final Stream<ResourceLocation>... files) {
return Arrays.stream(files).flatMap(Function.identity()).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList();
}
public interface GeneratorSink {

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.data;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import dan200.computercraft.shared.util.PrettyJsonWriter;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;

View File

@@ -148,7 +148,7 @@ class TagProvider {
/**
* A wrapper over {@link ItemTagsProvider}.
*/
interface ItemTagConsumer extends TagConsumer<Item> {
public interface ItemTagConsumer extends TagConsumer<Item> {
void copy(TagKey<Block> block, TagKey<Item> item);
}
}

View File

@@ -0,0 +1,4 @@
{
"type": "examplemod:example_turtle_upgrade",
"item": "minecraft:compass"
}

View File

@@ -0,0 +1,75 @@
package com.example.examplemod;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.turtle.ITurtleAccess;
import org.jetbrains.annotations.Nullable;
/**
* An example API that will be available on every turtle. This demonstrates both registering an API, and how to write
* Lua-facing functions.
* <p>
* This API is not available as a global (as {@link #getNames() returns nothing}), but is instead accessible via
* {@code require} (see {@link #getModuleName()}).
*
* <h2>Example</h2>
* <pre class="language language-lua">{@code
* local my_api = require("example.my_api")
* print("Turtle is facing " .. my_api.getDirection())
* }</pre>
*/
public class ExampleAPI implements ILuaAPI {
private final ITurtleAccess turtle;
public ExampleAPI(ITurtleAccess turtle) {
this.turtle = turtle;
}
public static void register() {
// @start region=register
ComputerCraftAPI.registerAPIFactory(computer -> {
// Read the turtle component.
var turtle = computer.getComponent(ComputerComponents.TURTLE);
// If present then add our API.
return turtle == null ? null : new ExampleAPI(turtle);
});
// @end region=register
}
@Override
public String[] getNames() {
return new String[0];
}
@Override
public @Nullable String getModuleName() {
return "example.my_api";
}
/**
* A Lua-facing function function that returns the direction the turtle is facing.
*
* @return The turtle's direction.
*/
@LuaFunction
public final String getDirection() {
return turtle.getDirection().getName();
}
/**
* A Lua-facing function using {@link Coerced}. Unlike a {@link LuaFunction} taking a raw {@link String}, this will
* accept any value, and convert it to a string.
*
* @param myString The value to write.
*/
// @start region=coerced
@LuaFunction
public final void writeString(Coerced<String> myString) {
String contents = myString.value();
System.out.println("Got " + contents);
}
// @end region=coerced
}

View File

@@ -0,0 +1,39 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleDataProvider;
import com.example.examplemod.peripheral.FurnacePeripheral;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
/**
* Our example mod, containing the various things we register.
* <p>
* This isn't an especially good template to follow! It's convenient for our example mod (as we need to be multi-loader
* compatible), but there's a good chance there's a better pattern to follow. For example, on Forge you'd use
* {@code DeferredRegister} to register things), and multi-loader mods probably have their own abstractions.
* <p>
* See {@code FabricExampleMod} and {@code ForgeExampleMod} for the actual mod entrypoints.
*/
public final class ExampleMod {
public static final String MOD_ID = "examplemod";
/**
* The upgrade serialiser for our example turtle upgrade. See the documentation for {@link TurtleUpgradeSerialiser}
* or {@code FabricExampleMod}/{@code ForgeExampleMod} for how this is registered.
* <p>
* This only defines the upgrade type. See {@link TurtleDataProvider} for defining the actual upgrade.
*/
// @start region=turtle_upgrades
public static final TurtleUpgradeSerialiser<ExampleTurtleUpgrade> EXAMPLE_TURTLE_UPGRADE = TurtleUpgradeSerialiser.simpleWithCustomItem(
ExampleTurtleUpgrade::new
);
// @end region=turtle_upgrades
public static void registerComputerCraft() {
// @start region=generic_source
ComputerCraftAPI.registerGenericSource(new FurnacePeripheral());
// @end region=generic_source
ExampleAPI.register();
}
}

View File

@@ -0,0 +1,17 @@
package com.example.examplemod;
import dan200.computercraft.api.turtle.AbstractTurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeType;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/**
* An example turtle upgrade.
*/
// @start region=body
public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade {
public ExampleTurtleUpgrade(ResourceLocation id, ItemStack stack) {
super(id, TurtleUpgradeType.PERIPHERAL, stack);
}
}
// @end region=body

View File

@@ -0,0 +1,17 @@
package com.example.examplemod.data;
import net.minecraft.data.DataGenerator;
import net.minecraft.data.DataProvider;
/**
* The entry point to example mod's data-generators.
* <p>
* This is called by our platform-specific entry-point (see {@code FabricExampleModDataGenerator} and
* {@code ForgeExampleModDataGenerator}. That said, the exact setup isn't relevant (it will vary depending on
* mod-loader), what's interesting is the contents of the {@link #run(DataGenerator.PackGenerator)} method!
*/
public final class ExampleModDataGenerators {
public static void run(DataGenerator.PackGenerator pack) {
pack.addProvider((DataProvider.Factory<?>) TurtleDataProvider::new);
}
}

View File

@@ -0,0 +1,34 @@
package com.example.examplemod.data;
import com.example.examplemod.ExampleMod;
import com.example.examplemod.ExampleTurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;
import java.util.function.Consumer;
/**
* A {@link TurtleUpgradeDataProvider} that generates the JSON for our {@linkplain ExampleTurtleUpgrade example
* upgrade}.
*
* @see ExampleModDataGenerators
*/
// @start region=body
public class TurtleDataProvider extends TurtleUpgradeDataProvider {
public TurtleDataProvider(PackOutput output) {
super(output);
}
@Override
protected void addUpgrades(Consumer<Upgrade<TurtleUpgradeSerialiser<?>>> addUpgrade) {
simpleWithCustomItem(
new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"),
ExampleMod.EXAMPLE_TURTLE_UPGRADE,
Items.COMPASS
).add(addUpgrade);
}
}
// @end region=body

View File

@@ -0,0 +1,12 @@
@ApiStatus.Internal
@DefaultQualifier(value = NonNull.class, locations = {
TypeUseLocation.RETURN,
TypeUseLocation.PARAMETER,
TypeUseLocation.FIELD,
})
package com.example.examplemod;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.checkerframework.framework.qual.TypeUseLocation;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,39 @@
package com.example.examplemod.peripheral;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
import org.jetbrains.annotations.Nullable;
/**
* A peripheral that adds a {@code getFuel()} method to brewing stands. This demonstrates the usage of
* {@link IPeripheral}.
*
* @see dan200.computercraft.api.peripheral
* @see FurnacePeripheral Using {@code GenericPeripheral}.
*/
// @start region=body
public class BrewingStandPeripheral implements IPeripheral {
private final BrewingStandBlockEntity brewingStand;
public BrewingStandPeripheral(BrewingStandBlockEntity brewingStand) {
this.brewingStand = brewingStand;
}
@Override
public String getType() {
return "brewing_stand";
}
@LuaFunction
public final int getFuel() {
// Don't do it this way! Use an access widener/transformer to access the "fuel" field instead.
return brewingStand.saveWithoutMetadata().getInt("Fuel");
}
@Override
public boolean equals(@Nullable IPeripheral other) {
return other instanceof BrewingStandPeripheral o && brewingStand == o.brewingStand;
}
}
// @end region=body

View File

@@ -0,0 +1,44 @@
package com.example.examplemod.peripheral;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.AttachedComputerSet;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import org.jetbrains.annotations.Nullable;
/**
* A peripheral that tracks what computers it is attached to.
*
* @see AttachedComputerSet
*/
// @start region=body
public class ComputerTrackingPeripheral implements IPeripheral {
private final AttachedComputerSet computers = new AttachedComputerSet();
@Override
public void attach(IComputerAccess computer) {
computers.add(computer);
}
@Override
public void detach(IComputerAccess computer) {
computers.remove(computer);
}
@LuaFunction
public final void sayHello() {
// Queue a "hello" event on each computer.
computers.forEach(x -> x.queueEvent("hello", x.getAttachmentName()));
}
@Override
public String getType() {
return "my_peripheral";
}
@Override
public boolean equals(@Nullable IPeripheral other) {
return this == other;
}
}
// @end region=body

View File

@@ -0,0 +1,29 @@
package com.example.examplemod.peripheral;
import com.example.examplemod.ExampleMod;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
/**
* A peripheral that adds a {@code getBurnTime} method to furnaces. This is used to demonstrate the usage of
* {@link GenericPeripheral}.
*
* @see dan200.computercraft.api.peripheral
* @see BrewingStandPeripheral Using {@code IPeripheral}.
*/
// @start region=body
public class FurnacePeripheral implements GenericPeripheral {
@Override
public String id() {
return new ResourceLocation(ExampleMod.MOD_ID, "furnace").toString();
}
@LuaFunction(mainThread = true)
public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
// Don't do it this way! Use an access widener/transformer to access the "litTime" field instead.
return furnace.saveWithoutMetadata().getInt("BurnTime");
}
}
// @end region=body

View File

@@ -1,11 +1,13 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/turtle_normal_selected_slot"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/turtle_advanced_selected_slot"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_off"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_off_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_on"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_on_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/terminate"},
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/terminate_hover"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},

View File

@@ -83,35 +83,35 @@
"gui.computercraft.config.disabled_generic_methods.tooltip": "A list of generic methods or method sources to disable. Generic methods are\nmethods added to a block/block entity when there is no explicit peripheral\nprovider. This includes inventory methods (i.e. inventory.getItemDetail,\ninventory.pushItems), and (if on Forge), the fluid_storage and energy_storage\nmethods.\nMethods in this list can either be a whole group of methods (computercraft:inventory)\nor a single method (computercraft:inventory#pushItems).\n",
"gui.computercraft.config.execution": "Execution",
"gui.computercraft.config.execution.computer_threads": "Computer threads",
"gui.computercraft.config.execution.computer_threads.tooltip": "Set the number of threads computers can run on. A higher number means more\ncomputers can run at once, but may induce lag. Please note that some mods may\nnot work with a thread count higher than 1. Use with caution.\nRange: > 1",
"gui.computercraft.config.execution.computer_threads.tooltip": "Set the number of threads computers can run on. A higher number means more\ncomputers can run at once, but may induce lag. Please note that some mods may\nnot work with a thread count higher than 1. Use with caution.",
"gui.computercraft.config.execution.max_main_computer_time": "Server tick computer time limit",
"gui.computercraft.config.execution.max_main_computer_time.tooltip": "The ideal maximum time a computer can execute for in a tick, in milliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.\nRange: > 1",
"gui.computercraft.config.execution.max_main_computer_time.tooltip": "The ideal maximum time a computer can execute for in a tick, in milliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.",
"gui.computercraft.config.execution.max_main_global_time": "Server tick global time limit",
"gui.computercraft.config.execution.max_main_global_time.tooltip": "The maximum time that can be spent executing tasks in a single tick, in\nmilliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.\nRange: > 1",
"gui.computercraft.config.execution.max_main_global_time.tooltip": "The maximum time that can be spent executing tasks in a single tick, in\nmilliseconds.\nNote, we will quite possibly go over this limit, as there's no way to tell how\nlong a will take - this aims to be the upper bound of the average time.",
"gui.computercraft.config.execution.tooltip": "Controls execution behaviour of computers. This is largely intended for\nfine-tuning servers, and generally shouldn't need to be touched.",
"gui.computercraft.config.floppy_space_limit": "Floppy Disk space limit (bytes)",
"gui.computercraft.config.floppy_space_limit.tooltip": "The disk space limit for floppy disks, in bytes.",
"gui.computercraft.config.http": "HTTP",
"gui.computercraft.config.http.bandwidth": "Bandwidth",
"gui.computercraft.config.http.bandwidth.global_download": "Global download limit",
"gui.computercraft.config.http.bandwidth.global_download.tooltip": "The number of bytes which can be downloaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1",
"gui.computercraft.config.http.bandwidth.global_download.tooltip": "The number of bytes which can be downloaded in a second. This is shared across all computers. (bytes/s).",
"gui.computercraft.config.http.bandwidth.global_upload": "Global upload limit",
"gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).\nRange: > 1",
"gui.computercraft.config.http.bandwidth.global_upload.tooltip": "The number of bytes which can be uploaded in a second. This is shared across all computers. (bytes/s).",
"gui.computercraft.config.http.bandwidth.tooltip": "Limits bandwidth used by computers.",
"gui.computercraft.config.http.enabled": "Enable the HTTP API",
"gui.computercraft.config.http.enabled.tooltip": "Enable the \"http\" API on Computers. Disabling this also disables the \"pastebin\" and\n\"wget\" programs, that many users rely on. It's recommended to leave this on and use\nthe \"rules\" config option to impose more fine-grained control.",
"gui.computercraft.config.http.max_requests": "Maximum concurrent requests",
"gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.\nRange: > 0",
"gui.computercraft.config.http.max_requests.tooltip": "The number of http requests a computer can make at one time. Additional requests\nwill be queued, and sent when the running requests have finished. Set to 0 for\nunlimited.",
"gui.computercraft.config.http.max_websockets": "Maximum concurrent websockets",
"gui.computercraft.config.http.max_websockets.tooltip": "The number of websockets a computer can have open at one time.\nRange: > 1",
"gui.computercraft.config.http.max_websockets.tooltip": "The number of websockets a computer can have open at one time.",
"gui.computercraft.config.http.proxy": "Proxy",
"gui.computercraft.config.http.proxy.host": "Host name",
"gui.computercraft.config.http.proxy.host.tooltip": "The hostname or IP address of the proxy server.",
"gui.computercraft.config.http.proxy.port": "Port",
"gui.computercraft.config.http.proxy.port.tooltip": "The port of the proxy server.\nRange: 1 ~ 65536",
"gui.computercraft.config.http.proxy.port.tooltip": "The port of the proxy server.",
"gui.computercraft.config.http.proxy.tooltip": "Tunnels HTTP and websocket requests through a proxy server. Only affects HTTP\nrules with \"use_proxy\" set to true (off by default).\nIf authentication is required for the proxy, create a \"computercraft-proxy.pw\"\nfile in the same directory as \"computercraft-server.toml\", containing the\nusername and password separated by a colon, e.g. \"myuser:mypassword\". For\nSOCKS4 proxies only the username is required.",
"gui.computercraft.config.http.proxy.type": "Proxy type",
"gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.\nAllowed Values: HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.config.http.proxy.type.tooltip": "The type of proxy to use.",
"gui.computercraft.config.http.rules": "Allow/deny rules",
"gui.computercraft.config.http.rules.tooltip": "A list of rules which control behaviour of the \"http\" API for specific domains or\nIPs. Each rule matches against a hostname and an optional port, and then sets several\nproperties for the request. Rules are evaluated in order, meaning earlier rules override\nlater ones.\n\nValid properties:\n - \"host\" (required): The domain or IP address this rule matches. This may be a domain name\n (\"pastebin.com\"), wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\").\n - \"port\" (optional): Only match requests for a specific port, such as 80 or 443.\n\n - \"action\" (optional): Whether to allow or deny this request.\n - \"max_download\" (optional): The maximum size (in bytes) that a computer can download in this\n request.\n - \"max_upload\" (optional): The maximum size (in bytes) that a computer can upload in a this request.\n - \"max_websocket_message\" (optional): The maximum size (in bytes) that a computer can send or\n receive in one websocket packet.\n - \"use_proxy\" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.",
"gui.computercraft.config.http.tooltip": "Controls the HTTP API",
@@ -120,61 +120,61 @@
"gui.computercraft.config.log_computer_errors": "Log computer errors",
"gui.computercraft.config.log_computer_errors.tooltip": "Log exceptions thrown by peripherals and other Lua objects. This makes it easier\nfor mod authors to debug problems, but may result in log spam should people use\nbuggy methods.",
"gui.computercraft.config.maximum_open_files": "Maximum files open per computer",
"gui.computercraft.config.maximum_open_files.tooltip": "Set how many files a computer can have open at the same time. Set to 0 for unlimited.\nRange: > 0",
"gui.computercraft.config.maximum_open_files.tooltip": "Set how many files a computer can have open at the same time. Set to 0 for unlimited.",
"gui.computercraft.config.monitor_distance": "Monitor distance",
"gui.computercraft.config.monitor_distance.tooltip": "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors.\nRange: 16 ~ 1024",
"gui.computercraft.config.monitor_distance.tooltip": "The maximum distance monitors will render at. This defaults to the standard tile\nentity limit, but may be extended if you wish to build larger monitors.",
"gui.computercraft.config.monitor_renderer": "Monitor renderer",
"gui.computercraft.config.monitor_renderer.tooltip": "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers.\nAllowed Values: BEST, TBO, VBO",
"gui.computercraft.config.monitor_renderer.tooltip": "The renderer to use for monitors. Generally this should be kept at \"best\" - if\nmonitors have performance issues, you may wish to experiment with alternative\nrenderers.",
"gui.computercraft.config.peripheral": "Peripherals",
"gui.computercraft.config.peripheral.command_block_enabled": "Enable command block peripheral",
"gui.computercraft.config.peripheral.command_block_enabled.tooltip": "Enable Command Block peripheral support",
"gui.computercraft.config.peripheral.max_notes_per_tick": "Maximum notes that a computer can play at once",
"gui.computercraft.config.peripheral.max_notes_per_tick.tooltip": "Maximum amount of notes a speaker can play at once.\nRange: > 1",
"gui.computercraft.config.peripheral.max_notes_per_tick.tooltip": "Maximum amount of notes a speaker can play at once.",
"gui.computercraft.config.peripheral.modem_high_altitude_range": "Modem range (high-altitude)",
"gui.computercraft.config.peripheral.modem_high_altitude_range.tooltip": "The range of Wireless Modems at maximum altitude in clear weather, in meters.\nRange: 0 ~ 100000",
"gui.computercraft.config.peripheral.modem_high_altitude_range.tooltip": "The range of Wireless Modems at maximum altitude in clear weather, in meters.",
"gui.computercraft.config.peripheral.modem_high_altitude_range_during_storm": "Modem range (high-altitude, bad weather)",
"gui.computercraft.config.peripheral.modem_high_altitude_range_during_storm.tooltip": "The range of Wireless Modems at maximum altitude in stormy weather, in meters.\nRange: 0 ~ 100000",
"gui.computercraft.config.peripheral.modem_high_altitude_range_during_storm.tooltip": "The range of Wireless Modems at maximum altitude in stormy weather, in meters.",
"gui.computercraft.config.peripheral.modem_range": "Modem range (default)",
"gui.computercraft.config.peripheral.modem_range.tooltip": "The range of Wireless Modems at low altitude in clear weather, in meters.\nRange: 0 ~ 100000",
"gui.computercraft.config.peripheral.modem_range.tooltip": "The range of Wireless Modems at low altitude in clear weather, in meters.",
"gui.computercraft.config.peripheral.modem_range_during_storm": "Modem range (bad weather)",
"gui.computercraft.config.peripheral.modem_range_during_storm.tooltip": "The range of Wireless Modems at low altitude in stormy weather, in meters.\nRange: 0 ~ 100000",
"gui.computercraft.config.peripheral.modem_range_during_storm.tooltip": "The range of Wireless Modems at low altitude in stormy weather, in meters.",
"gui.computercraft.config.peripheral.monitor_bandwidth": "Monitor bandwidth",
"gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "The limit to how much monitor data can be sent *per tick*. Note:\n - Bandwidth is measured before compression, so the data sent to the client is\n smaller.\n - This ignores the number of players a packet is sent to. Updating a monitor for\n one player consumes the same bandwidth limit as sending to 20.\n - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40\n monitors to be updated in a single tick.\nSet to 0 to disable.\nRange: > 0",
"gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "The limit to how much monitor data can be sent *per tick*. Note:\n - Bandwidth is measured before compression, so the data sent to the client is\n smaller.\n - This ignores the number of players a packet is sent to. Updating a monitor for\n one player consumes the same bandwidth limit as sending to 20.\n - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40\n monitors to be updated in a single tick.\nSet to 0 to disable.",
"gui.computercraft.config.peripheral.tooltip": "Various options relating to peripherals.",
"gui.computercraft.config.term_sizes": "Terminal sizes",
"gui.computercraft.config.term_sizes.computer": "Computer",
"gui.computercraft.config.term_sizes.computer.height": "Terminal height",
"gui.computercraft.config.term_sizes.computer.height.tooltip": "Range: 1 ~ 255",
"gui.computercraft.config.term_sizes.computer.height.tooltip": "Height of computer terminal",
"gui.computercraft.config.term_sizes.computer.tooltip": "Terminal size of computers.",
"gui.computercraft.config.term_sizes.computer.width": "Terminal width",
"gui.computercraft.config.term_sizes.computer.width.tooltip": "Range: 1 ~ 255",
"gui.computercraft.config.term_sizes.computer.width.tooltip": "Width of computer terminal",
"gui.computercraft.config.term_sizes.monitor": "Monitor",
"gui.computercraft.config.term_sizes.monitor.height": "Max monitor height",
"gui.computercraft.config.term_sizes.monitor.height.tooltip": "Range: 1 ~ 32",
"gui.computercraft.config.term_sizes.monitor.height.tooltip": "Maximum height of monitors",
"gui.computercraft.config.term_sizes.monitor.tooltip": "Maximum size of monitors (in blocks).",
"gui.computercraft.config.term_sizes.monitor.width": "Max monitor width",
"gui.computercraft.config.term_sizes.monitor.width.tooltip": "Range: 1 ~ 32",
"gui.computercraft.config.term_sizes.monitor.width.tooltip": "Maximum width of monitors",
"gui.computercraft.config.term_sizes.pocket_computer": "Pocket Computer",
"gui.computercraft.config.term_sizes.pocket_computer.height": "Terminal height",
"gui.computercraft.config.term_sizes.pocket_computer.height.tooltip": "Range: 1 ~ 255",
"gui.computercraft.config.term_sizes.pocket_computer.height.tooltip": "Height of pocket computer terminal",
"gui.computercraft.config.term_sizes.pocket_computer.tooltip": "Terminal size of pocket computers.",
"gui.computercraft.config.term_sizes.pocket_computer.width": "Terminal width",
"gui.computercraft.config.term_sizes.pocket_computer.width.tooltip": "Range: 1 ~ 255",
"gui.computercraft.config.term_sizes.pocket_computer.width.tooltip": "Width of pocket computer terminal",
"gui.computercraft.config.term_sizes.tooltip": "Configure the size of various computer's terminals.\nLarger terminals require more bandwidth, so use with care.",
"gui.computercraft.config.turtle": "Turtles",
"gui.computercraft.config.turtle.advanced_fuel_limit": "Advanced Turtle fuel limit",
"gui.computercraft.config.turtle.advanced_fuel_limit.tooltip": "The fuel limit for Advanced Turtles.\nRange: > 0",
"gui.computercraft.config.turtle.advanced_fuel_limit.tooltip": "The fuel limit for Advanced Turtles.",
"gui.computercraft.config.turtle.can_push": "Turtles can push entities",
"gui.computercraft.config.turtle.can_push.tooltip": "If set to true, Turtles will push entities out of the way instead of stopping if\nthere is space to do so.",
"gui.computercraft.config.turtle.need_fuel": "Enable fuel",
"gui.computercraft.config.turtle.need_fuel.tooltip": "Set whether Turtles require fuel to move.",
"gui.computercraft.config.turtle.normal_fuel_limit": "Turtle fuel limit",
"gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "The fuel limit for Turtles.\nRange: > 0",
"gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "The fuel limit for Turtles.",
"gui.computercraft.config.turtle.tooltip": "Various options relating to turtles.",
"gui.computercraft.config.upload_max_size": "File upload size limit (bytes)",
"gui.computercraft.config.upload_max_size.tooltip": "The file upload size limit, in bytes. Must be in range of 1 KiB and 16 MiB.\nKeep in mind that uploads are processed in a single tick - large files or\npoor network performance can stall the networking thread. And mind the disk space!\nRange: 1024 ~ 16777216",
"gui.computercraft.config.upload_max_size.tooltip": "The file upload size limit, in bytes. Must be in range of 1 KiB and 16 MiB.\nKeep in mind that uploads are processed in a single tick - large files or\npoor network performance can stall the networking thread. And mind the disk space!",
"gui.computercraft.config.upload_nag_delay": "Upload nag delay",
"gui.computercraft.config.upload_nag_delay.tooltip": "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable.\nRange: 0 ~ 60",
"gui.computercraft.config.upload_nag_delay.tooltip": "The delay in seconds after which we'll notify about unhandled imports. Set to 0 to disable.",
"gui.computercraft.pocket_computer_overlay": "Pocket computer open. Press ESC to close.",
"gui.computercraft.terminal": "Computer terminal",
"gui.computercraft.tooltip.computer_id": "Computer ID: %s",

View File

@@ -2,6 +2,11 @@
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"},
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"},
{"type": "minecraft:single", "resource": "computercraft:entity/printout"}
{"type": "minecraft:single", "resource": "computercraft:entity/printout"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_normal"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_advanced"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_colour"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_frame"},
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_light"}
]
}

View File

@@ -12,6 +12,7 @@ import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.api.media.PrintoutContents;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
@@ -26,6 +27,7 @@ import dan200.computercraft.shared.computer.core.ResourceMount;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.details.BlockDetails;
import dan200.computercraft.shared.details.ItemDetails;
import dan200.computercraft.shared.media.items.PrintoutItem;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
@@ -39,6 +41,7 @@ import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.stream.Stream;
public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIService {
private final DetailRegistry<ItemStack> itemStackDetails = new DetailRegistryImpl<>(ItemDetails::fillBasic);
@@ -134,4 +137,21 @@ public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIServic
public final DetailRegistry<BlockReference> getBlockInWorldDetailRegistry() {
return blockDetails;
}
@Override
public @Nullable PrintoutContents getPrintoutContents(ItemStack stack) {
return stack.getItem() instanceof PrintoutItem ? new PrintoutContentsImpl(stack) : null;
}
public record PrintoutContentsImpl(ItemStack stack) implements PrintoutContents {
@Override
public String getTitle() {
return PrintoutItem.getTitle(stack);
}
@Override
public Stream<String> getTextLines() {
return Stream.of(PrintoutItem.getText(stack));
}
}
}

View File

@@ -28,6 +28,7 @@ import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
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.core.ServerComputer;
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
import dan200.computercraft.shared.computer.items.CommandComputerItem;
import dan200.computercraft.shared.computer.items.ComputerItem;
@@ -89,7 +90,6 @@ 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;
@@ -471,7 +471,7 @@ public final class ModRegistry {
ComputerCraftAPI.registerAPIFactory(computer -> {
var turtle = computer.getComponent(ComputerComponents.TURTLE);
var metrics = Objects.requireNonNull(computer.getComponent(ComponentMap.METRICS));
var metrics = Objects.requireNonNull(computer.getComponent(ServerComputer.METRICS));
return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle);
});

View File

@@ -24,15 +24,13 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.RelativeMovement;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
@@ -260,18 +258,11 @@ public final class CommandComputerCraft {
* @return The constant {@code 1}.
*/
private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException {
var player = source.getPlayerOrException();
new ComputerContainerData(computer, ItemStack.EMPTY).open(player, new MenuProvider() {
@Override
public Component getDisplayName() {
return Component.translatable("gui.computercraft.view_computer");
}
@Override
public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer);
}
});
PlatformHelper.get().openMenu(
source.getPlayerOrException(), Component.translatable("gui.computercraft.view_computer"),
(id, player, entity) -> new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer),
new ComputerContainerData(computer, ItemStack.EMPTY)
);
return 1;
}

View File

@@ -9,6 +9,7 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.network.container.ComputerContainerData;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos;
@@ -161,7 +162,7 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
var serverComputer = computer.createServerComputer();
serverComputer.turnOn();
new ComputerContainerData(serverComputer, getItem(computer)).open(player, computer);
PlatformHelper.get().openMenu(player, computer.getName(), computer, new ComputerContainerData(serverComputer, getItem(computer)));
}
return InteractionResult.sidedSuccess(level.isClientSide);
}

View File

@@ -26,9 +26,10 @@ import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.Container;
import net.minecraft.world.LockCode;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.Nameable;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -38,7 +39,7 @@ import javax.annotation.Nullable;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuProvider {
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuConstructor {
private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On";
@@ -310,6 +311,10 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return label;
}
public final boolean isAdminOnly() {
return getBlockState().getBlock() instanceof GameMasterBlock;
}
public final void setComputerID(int id) {
if (getLevel().isClientSide || computerID == id) return;
@@ -417,4 +422,9 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
public Component getDisplayName() {
return Nameable.super.getDisplayName();
}
@Override
public boolean onlyOpCanSetNbt() {
return isAdminOnly();
}
}

View File

@@ -11,8 +11,7 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
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 dan200.computercraft.shared.config.ConfigSpec;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@@ -33,10 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Override
protected ServerComputer createComputer(int id) {
return new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
getFamily(), Config.computerTermWidth, Config.computerTermHeight,
ComponentMap.empty()
return new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
.label(getLabel())
.terminalSize(ConfigSpec.computerTermWidth.get(), ConfigSpec.computerTermHeight.get())
);
}

View File

@@ -11,7 +11,6 @@ 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;
@@ -26,11 +25,11 @@ import java.util.Map;
final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle {
private final ServerComputer computer;
private final IAPIEnvironment environment;
private final ComponentMap components;
private final Map<ComputerComponent<?>, Object> components;
private boolean active;
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, ComponentMap components) {
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, Map<ComputerComponent<?>, Object> components) {
super(environment);
this.computer = computer;
this.environment = environment;
@@ -95,7 +94,8 @@ final class ComputerSystem extends ComputerAccess implements IComputerSystem, Ap
}
@Override
@SuppressWarnings("unchecked")
public <T> @Nullable T getComponent(ComputerComponent<T> component) {
return components.get(component);
return (T) components.get(component);
}
}

View File

@@ -6,44 +6,33 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* Handles user-provided input, forwarding it to a computer. This is used
* Handles user-provided input, forwarding it to a computer. This describes the "shape" of both the client-and
* server-side input handlers.
*
* @see ServerInputHandler
* @see ServerComputer
*/
public interface InputHandler {
void queueEvent(String event, @Nullable Object[] arguments);
void keyDown(int key, boolean repeat);
default void queueEvent(String event) {
queueEvent(event, null);
}
void keyUp(int key);
default void keyDown(int key, boolean repeat) {
queueEvent("key", new Object[]{ key, repeat });
}
void charTyped(byte chr);
default void keyUp(int key) {
queueEvent("key_up", new Object[]{ key });
}
void paste(ByteBuffer contents);
default void mouseClick(int button, int x, int y) {
queueEvent("mouse_click", new Object[]{ button, x, y });
}
void mouseClick(int button, int x, int y);
default void mouseUp(int button, int x, int y) {
queueEvent("mouse_up", new Object[]{ button, x, y });
}
void mouseUp(int button, int x, int y);
default void mouseDrag(int button, int x, int y) {
queueEvent("mouse_drag", new Object[]{ button, x, y });
}
void mouseDrag(int button, int x, int y);
default void mouseScroll(int direction, int x, int y) {
queueEvent("mouse_scroll", new Object[]{ direction, x, y });
}
void mouseScroll(int direction, int x, int y);
void terminate();
void shutdown();

View File

@@ -6,12 +6,14 @@ package dan200.computercraft.shared.computer.core;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.AdminComputer;
import dan200.computercraft.api.component.ComputerComponent;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ComputerEnvironment;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.impl.ApiFactories;
@@ -23,18 +25,21 @@ 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;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment {
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
private final int instanceID;
private final UUID instanceUUID = UUID.randomUUID();
@@ -50,30 +55,25 @@ 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,
ComponentMap baseComponents
) {
public ServerComputer(ServerLevel level, BlockPos position, Properties properties) {
this.level = level;
this.position = position;
this.family = family;
this.family = properties.family;
var context = ServerContext.get(level.getServer());
instanceID = context.registry().getUnusedInstanceID();
terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
terminal = new NetworkedTerminal(properties.terminalWidth, properties.terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
metrics = context.metrics().createMetricObserver(this);
var componentBuilder = ComponentMap.builder();
componentBuilder.add(ComponentMap.METRICS, metrics);
properties.addComponent(METRICS, metrics);
if (family == ComputerFamily.COMMAND) {
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
properties.addComponent(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
});
}
componentBuilder.add(baseComponents);
var components = componentBuilder.build();
var components = Map.copyOf(properties.components);
computer = new Computer(context.computerContext(), this, terminal, computerID);
computer.setLabel(label);
computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
computer.setLabel(properties.label);
// Load in the externally registered APIs.
for (var factory : ApiFactories.getAll()) {
@@ -86,24 +86,24 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
}
}
public ComputerFamily getFamily() {
public final ComputerFamily getFamily() {
return family;
}
public ServerLevel getLevel() {
public final ServerLevel getLevel() {
return level;
}
public BlockPos getPosition() {
public final BlockPos getPosition() {
return position;
}
public void setPosition(ServerLevel level, BlockPos pos) {
public final void setPosition(ServerLevel level, BlockPos pos) {
this.level = level;
position = pos.immutable();
}
protected void markTerminalChanged() {
protected final void markTerminalChanged() {
terminalChanged.set(true);
}
@@ -117,11 +117,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState()));
}
public TerminalState getTerminalState() {
return new TerminalState(terminal);
public final TerminalState getTerminalState() {
return TerminalState.create(terminal);
}
public void keepAlive() {
public final void keepAlive() {
ticksSincePing = 0;
}
@@ -134,7 +134,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
*
* @return What sides on the computer have changed.
*/
public int pollRedstoneChanges() {
public final int pollRedstoneChanges() {
return computer.pollRedstoneChanges();
}
@@ -147,7 +147,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
computer.unload();
}
public void close() {
public final void close() {
unload();
ServerContext.get(level.getServer()).registry().remove(this);
}
@@ -167,7 +167,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
var server = level.getServer();
for (var player : server.getPlayerList().getPlayers()) {
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
if (player.containerMenu instanceof ComputerMenu menu && menu.getComputer() == this) {
ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
}
}
@@ -176,97 +176,135 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
protected void onRemoved() {
}
public int getInstanceID() {
public final int getInstanceID() {
return instanceID;
}
public UUID getInstanceUUID() {
public final UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() {
public final int getID() {
return computer.getID();
}
public @Nullable String getLabel() {
public final @Nullable String getLabel() {
return computer.getLabel();
}
public boolean isOn() {
public final boolean isOn() {
return computer.isOn();
}
public ComputerState getState() {
public final ComputerState getState() {
if (!computer.isOn()) return ComputerState.OFF;
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
}
@Override
public void turnOn() {
public final void turnOn() {
computer.turnOn();
}
@Override
public void shutdown() {
public final void shutdown() {
computer.shutdown();
}
@Override
public void reboot() {
public final void reboot() {
computer.reboot();
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
public final void queueEvent(String event, @Nullable Object[] arguments) {
computer.queueEvent(event, arguments);
}
public int getRedstoneOutput(ComputerSide side) {
public final void queueEvent(String event) {
queueEvent(event, null);
}
public final int getRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0;
}
public void setRedstoneInput(ComputerSide side, int level, int bundledState) {
public final void setRedstoneInput(ComputerSide side, int level, int bundledState) {
computer.getRedstone().setInput(side, level, bundledState);
}
public int getBundledRedstoneOutput(ComputerSide side) {
public final int getBundledRedstoneOutput(ComputerSide side) {
return computer.isOn() ? computer.getRedstone().getExternalBundledOutput(side) : 0;
}
public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
public final void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
computer.getEnvironment().setPeripheral(side, peripheral);
}
@Nullable
public IPeripheral getPeripheral(ComputerSide side) {
public final IPeripheral getPeripheral(ComputerSide side) {
return computer.getEnvironment().getPeripheral(side);
}
public void setLabel(@Nullable String label) {
public final void setLabel(@Nullable String label) {
computer.setLabel(label);
}
@Override
public double getTimeOfDay() {
public final double getTimeOfDay() {
return (level.getDayTime() + 6000) % 24000 / 1000.0;
}
@Override
public int getDay() {
public final int getDay() {
return (int) ((level.getDayTime() + 6000) / 24000) + 1;
}
@Override
public MetricsObserver getMetrics() {
public final MetricsObserver getMetrics() {
return metrics;
}
public WorkMonitor getMainThreadMonitor() {
public final WorkMonitor getMainThreadMonitor() {
return computer.getMainThreadMonitor();
}
@Override
public @Nullable WritableMount createRootMount() {
public final WritableMount createRootMount() {
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit);
}
public static Properties properties(int computerID, ComputerFamily family) {
return new Properties(computerID, family);
}
public static final class Properties {
private final int computerID;
private @Nullable String label;
private final ComputerFamily family;
private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH;
private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT;
private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
private Properties(int computerID, ComputerFamily family) {
this.computerID = computerID;
this.family = family;
}
public Properties label(@Nullable String label) {
this.label = label;
return this;
}
public Properties terminalSize(int width, int height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("Terminal size must be positive");
this.terminalWidth = width;
this.terminalHeight = height;
return this;
}
public <T> Properties addComponent(ComputerComponent<T> component, T value) {
if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
components.put(component, value);
return this;
}
}
}

View File

@@ -7,6 +7,8 @@ package dan200.computercraft.shared.computer.menu;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.apis.transfer.TransferredFile;
import dan200.computercraft.core.apis.transfer.TransferredFiles;
import dan200.computercraft.core.computer.ComputerEvents;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
@@ -21,6 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.UUID;
@@ -48,21 +51,33 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
this.owner = owner;
}
@Override
public void queueEvent(String event, @Nullable Object[] arguments) {
owner.getComputer().queueEvent(event, arguments);
}
@Override
public void keyDown(int key, boolean repeat) {
keysDown.add(key);
owner.getComputer().keyDown(key, repeat);
ComputerEvents.keyDown(owner.getComputer(), key, repeat);
}
@Override
public void keyUp(int key) {
keysDown.remove(key);
owner.getComputer().keyUp(key);
ComputerEvents.keyUp(owner.getComputer(), key);
}
@Override
public void charTyped(byte chr) {
if (StringUtil.isTypableChar(chr)) ComputerEvents.charTyped(owner.getComputer(), chr);
}
@Override
public void paste(ByteBuffer contents) {
if (contents.remaining() > 0 && isValidClipboard(contents)) ComputerEvents.paste(owner.getComputer(), contents);
}
private static boolean isValidClipboard(ByteBuffer buffer) {
for (int i = buffer.remaining(), max = buffer.limit(); i < max; i++) {
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
}
return true;
}
@Override
@@ -71,7 +86,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = button;
owner.getComputer().mouseClick(button, x, y);
ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
}
@Override
@@ -80,7 +95,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = -1;
owner.getComputer().mouseUp(button, x, y);
ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
}
@Override
@@ -89,7 +104,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseY = y;
lastMouseDown = button;
owner.getComputer().mouseDrag(button, x, y);
ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
}
@Override
@@ -97,7 +112,12 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
lastMouseX = x;
lastMouseY = y;
owner.getComputer().mouseScroll(direction, x, y);
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
}
@Override
public void terminate() {
owner.getComputer().queueEvent("terminate");
}
@Override
@@ -169,9 +189,9 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
public void close() {
var computer = owner.getComputer();
var keys = keysDown.iterator();
while (keys.hasNext()) computer.keyUp(keys.nextInt());
while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt());
if (lastMouseDown != -1) computer.mouseUp(lastMouseDown, lastMouseX, lastMouseY);
if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY);
keysDown.clear();
lastMouseDown = -1;

View File

@@ -8,7 +8,6 @@ import dan200.computercraft.core.terminal.Palette;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Colour;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
public class NetworkedTerminal extends Terminal {
public NetworkedTerminal(int width, int height, boolean colour) {
@@ -19,59 +18,61 @@ public class NetworkedTerminal extends Terminal {
super(width, height, colour, changedCallback);
}
public synchronized void write(FriendlyByteBuf buffer) {
buffer.writeInt(cursorX);
buffer.writeInt(cursorY);
buffer.writeBoolean(cursorBlink);
buffer.writeByte(cursorBackgroundColour << 4 | cursorColour);
synchronized TerminalState write() {
var contents = new byte[width * height * 2 + Palette.PALETTE_SIZE * 3];
var idx = 0;
for (var y = 0; y < height; y++) {
var text = this.text[y];
var textColour = this.textColour[y];
var backColour = backgroundColour[y];
for (var x = 0; x < width; x++) buffer.writeByte(text.charAt(x) & 0xFF);
for (var x = 0; x < width; x++) contents[idx++] = (byte) (text.charAt(x) & 0xFF);
for (var x = 0; x < width; x++) {
buffer.writeByte(getColour(
backColour.charAt(x), Colour.BLACK) << 4 |
getColour(textColour.charAt(x), Colour.WHITE)
);
contents[idx++] = (byte) (getColour(backColour.charAt(x), Colour.BLACK) << 4 | getColour(textColour.charAt(x), Colour.WHITE));
}
}
for (var i = 0; i < Palette.PALETTE_SIZE; i++) {
for (var channel : palette.getColour(i)) buffer.writeByte((int) (channel * 0xFF) & 0xFF);
for (var channel : palette.getColour(i)) contents[idx++] = (byte) ((int) (channel * 0xFF) & 0xFF);
}
assert idx == contents.length;
return new TerminalState(colour, width, height, cursorX, cursorY, cursorBlink, cursorColour, cursorBackgroundColour, contents);
}
public synchronized void read(FriendlyByteBuf buffer) {
cursorX = buffer.readInt();
cursorY = buffer.readInt();
cursorBlink = buffer.readBoolean();
synchronized void read(TerminalState state) {
resize(state.width, state.height);
cursorX = state.cursorX;
cursorY = state.cursorY;
cursorBlink = state.cursorBlink;
var cursorColour = buffer.readByte();
cursorBackgroundColour = (cursorColour >> 4) & 0xF;
this.cursorColour = cursorColour & 0xF;
cursorBackgroundColour = state.cursorBgColour;
this.cursorColour = state.cursorFgColour;
var contents = state.contents;
var idx = 0;
for (var y = 0; y < height; y++) {
var text = this.text[y];
var textColour = this.textColour[y];
var backColour = backgroundColour[y];
for (var x = 0; x < width; x++) text.setChar(x, (char) (buffer.readByte() & 0xFF));
for (var x = 0; x < width; x++) text.setChar(x, (char) (contents[idx++] & 0xFF));
for (var x = 0; x < width; x++) {
var colour = buffer.readByte();
var colour = contents[idx++];
backColour.setChar(x, BASE_16.charAt((colour >> 4) & 0xF));
textColour.setChar(x, BASE_16.charAt(colour & 0xF));
}
}
for (var i = 0; i < Palette.PALETTE_SIZE; i++) {
var r = (buffer.readByte() & 0xFF) / 255.0;
var g = (buffer.readByte() & 0xFF) / 255.0;
var b = (buffer.readByte() & 0xFF) / 255.0;
var r = (contents[idx++] & 0xFF) / 255.0;
var g = (contents[idx++] & 0xFF) / 255.0;
var b = (contents[idx++] & 0xFF) / 255.0;
palette.setColour(i, r, g, b);
}
assert idx == contents.length;
setChanged();
}

View File

@@ -4,8 +4,6 @@
package dan200.computercraft.shared.computer.terminal;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.Contract;
@@ -20,53 +18,72 @@ import javax.annotation.Nullable;
*/
public class TerminalState {
private final boolean colour;
private final int width;
private final int height;
private final ByteBuf buffer;
final int width;
final int height;
final int cursorX;
final int cursorY;
final boolean cursorBlink;
final int cursorBgColour;
final int cursorFgColour;
final byte[] contents;
public TerminalState(NetworkedTerminal terminal) {
colour = terminal.isColour();
width = terminal.getWidth();
height = terminal.getHeight();
var buf = buffer = Unpooled.buffer();
terminal.write(new FriendlyByteBuf(buf));
TerminalState(
boolean colour, int width, int height, int cursorX, int cursorY, boolean cursorBlink, int cursorFgColour, int cursorBgColour, byte[] contents
) {
this.colour = colour;
this.width = width;
this.height = height;
this.cursorX = cursorX;
this.cursorY = cursorY;
this.cursorBlink = cursorBlink;
this.cursorFgColour = cursorFgColour;
this.cursorBgColour = cursorBgColour;
this.contents = contents;
}
@Contract("null -> null; !null -> !null")
public static @Nullable TerminalState create(@Nullable NetworkedTerminal terminal) {
return terminal == null ? null : new TerminalState(terminal);
return terminal == null ? null : terminal.write();
}
public TerminalState(FriendlyByteBuf buf) {
colour = buf.readBoolean();
width = buf.readVarInt();
height = buf.readVarInt();
cursorX = buf.readVarInt();
cursorY = buf.readVarInt();
cursorBlink = buf.readBoolean();
var length = buf.readVarInt();
buffer = buf.readBytes(length);
var cursorColour = buf.readByte();
this.cursorBgColour = (cursorColour >> 4) & 0xF;
this.cursorFgColour = cursorColour & 0xF;
contents = buf.readByteArray();
}
public void write(FriendlyByteBuf buf) {
buf.writeBoolean(colour);
buf.writeVarInt(width);
buf.writeVarInt(height);
buf.writeVarInt(buffer.readableBytes());
buf.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes());
buf.writeVarInt(cursorX);
buf.writeVarInt(cursorY);
buf.writeBoolean(cursorBlink);
buf.writeByte(cursorBgColour << 4 | cursorFgColour);
buf.writeByteArray(contents);
}
public int size() {
return buffer.readableBytes();
return contents.length;
}
public void apply(NetworkedTerminal terminal) {
terminal.resize(width, height);
terminal.read(new FriendlyByteBuf(buffer));
terminal.read(this);
}
public NetworkedTerminal create() {
var terminal = new NetworkedTerminal(width, height, colour);
terminal.read(new FriendlyByteBuf(buffer));
terminal.read(this);
return terminal;
}
}

View File

@@ -32,14 +32,14 @@ public final class Config {
public static int advancedTurtleFuelLimit = 100000;
public static boolean turtlesCanPush = true;
public static int computerTermWidth = 51;
public static int computerTermHeight = 19;
public static final int DEFAULT_COMPUTER_TERM_WIDTH = 51;
public static final int DEFAULT_COMPUTER_TERM_HEIGHT = 19;
public static final int turtleTermWidth = 39;
public static final int turtleTermHeight = 13;
public static final int TURTLE_TERM_WIDTH = 39;
public static final int TURTLE_TERM_HEIGHT = 13;
public static int pocketTermWidth = 26;
public static int pocketTermHeight = 20;
public static final int DEFAULT_POCKET_TERM_WIDTH = 26;
public static final int DEFAULT_POCKET_TERM_HEIGHT = 20;
public static int monitorWidth = 8;
public static int monitorHeight = 6;

View File

@@ -9,9 +9,7 @@ import com.google.common.base.Splitter;
import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.*;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -19,27 +17,47 @@ import java.util.stream.Stream;
/**
* A config file which the user can modify.
*/
public interface ConfigFile {
String TRANSLATION_PREFIX = "gui.computercraft.config.";
Splitter SPLITTER = Splitter.on('.');
public abstract class ConfigFile {
public static final String TRANSLATION_PREFIX = "gui.computercraft.config.";
public static final Splitter SPLITTER = Splitter.on('.');
/**
* An entry in the config file, either a {@link Value} or {@linkplain Group group of other entries}.
*/
sealed interface Entry permits Group, Value {
public abstract static sealed class Entry permits Group, Value {
protected final String path;
private final String translationKey;
private final String comment;
protected Entry(String path, String comment) {
this.path = path;
this.translationKey = TRANSLATION_PREFIX + path;
this.comment = comment;
}
public final String path() {
return path;
}
/**
* Get the translation key of this config entry.
*
* @return This entry's translation key.
*/
String translationKey();
public final String translationKey() {
return translationKey;
}
/**
* Get the comment about this config entry.
*
* @return The comment for this config entry.
*/
String comment();
public final String comment() {
return comment;
}
abstract Stream<Entry> entries();
}
/**
@@ -47,13 +65,38 @@ public interface ConfigFile {
*
* @param <T> The type of the stored value.
*/
non-sealed interface Value<T> extends Entry, Supplier<T> {
public abstract static non-sealed class Value<T> extends Entry implements Supplier<T> {
protected Value(String translationKey, String comment) {
super(translationKey, comment);
}
@Override
Stream<Entry> entries() {
return Stream.of(this);
}
}
/**
* A group of config entries.
*/
non-sealed interface Group extends Entry {
public static final class Group extends Entry {
private final Map<String, Entry> children;
public Group(String translationKey, String comment, Map<String, Entry> children) {
super(translationKey, comment);
this.children = children;
}
@Override
Stream<Entry> entries() {
return Stream.concat(Stream.of(this), children.values().stream().flatMap(Entry::entries));
}
}
protected final Map<String, Entry> entries;
protected ConfigFile(Map<String, Entry> entries) {
this.entries = entries;
}
/**
@@ -61,16 +104,46 @@ public interface ConfigFile {
*
* @return All config keys.
*/
Stream<Entry> entries();
public final Stream<Entry> entries() {
return entries.values().stream().flatMap(Entry::entries);
}
@Nullable
Entry getEntry(String path);
public final @Nullable Entry getEntry(String path) {
var iterator = SPLITTER.split(path).iterator();
var entry = entries.get(iterator.next());
while (iterator.hasNext()) {
if (!(entry instanceof Group group)) return null;
entry = group.children.get(iterator.next());
}
return entry;
}
/**
* A builder which can be used to generate a config object.
*/
abstract class Builder {
protected final Deque<String> groupStack = new ArrayDeque<>();
public abstract static class Builder {
protected record RootGroup(String path, Map<String, Entry> children) {
public RootGroup {
}
}
protected final Deque<RootGroup> groupStack = new ArrayDeque<>();
private @Nullable String pendingComment;
protected Builder() {
groupStack.addLast(new RootGroup("", new HashMap<>()));
}
protected final String getPath() {
return groupStack.getLast().path();
}
protected final String getPath(String name) {
var path = groupStack.getLast().path();
return path.isEmpty() ? name : path + "." + name;
}
protected String getTranslation(String name) {
var key = new StringBuilder(TRANSLATION_PREFIX);
@@ -86,7 +159,19 @@ public interface ConfigFile {
* @param comment The comment.
* @return The current object, for chaining.
*/
public abstract Builder comment(String comment);
@OverridingMethodsMustInvokeSuper
public ConfigFile.Builder comment(String comment) {
if (pendingComment != null) throw new IllegalStateException("Already have a comment");
pendingComment = comment;
return this;
}
protected String takeComment() {
var comment = pendingComment;
if (comment == null) throw new IllegalStateException("No comment specified");
pendingComment = null;
return comment;
}
/**
* Push a new config group.
@@ -95,7 +180,10 @@ public interface ConfigFile {
*/
@OverridingMethodsMustInvokeSuper
public void push(String name) {
groupStack.addLast(name);
var path = getPath(name);
Map<String, Entry> children = new HashMap<>();
groupStack.getLast().children().put(name, new Group(path, takeComment(), children));
groupStack.addLast(new RootGroup(path, children));
}
/**
@@ -113,22 +201,22 @@ public interface ConfigFile {
*/
public abstract Builder worldRestart();
public abstract <T> ConfigFile.Value<T> define(String path, T defaultValue);
public abstract <T> ConfigFile.Value<T> define(String name, T defaultValue);
/**
* A boolean-specific override of the above {@link #define(String, Object)} method.
*
* @param path The path to the value we're defining.
* @param name The name of the value we're defining.
* @param defaultValue The default value.
* @return The accessor for this config option.
*/
public abstract ConfigFile.Value<Boolean> define(String path, boolean defaultValue);
public abstract ConfigFile.Value<Boolean> define(String name, boolean defaultValue);
public abstract ConfigFile.Value<Integer> defineInRange(String path, int defaultValue, int min, int max);
public abstract ConfigFile.Value<Integer> defineInRange(String name, int defaultValue, int min, int max);
public abstract <T> ConfigFile.Value<List<? extends T>> defineList(String path, List<? extends T> defaultValue, Predicate<Object> elementValidator);
public abstract <T> ConfigFile.Value<List<? extends T>> defineList(String name, List<? extends T> defaultValue, Predicate<Object> elementValidator);
public abstract <V extends Enum<V>> ConfigFile.Value<V> defineEnum(String path, V defaultValue);
public abstract <V extends Enum<V>> ConfigFile.Value<V> defineEnum(String name, V defaultValue);
/**
* Finalise this config file.
@@ -140,7 +228,7 @@ public interface ConfigFile {
}
@FunctionalInterface
interface ConfigListener {
public interface ConfigListener {
/**
* The function called then a config file is changed.
*

View File

@@ -344,18 +344,18 @@ public final class ConfigSpec {
.push("term_sizes");
builder.comment("Terminal size of computers.").push("computer");
computerTermWidth = builder.defineInRange("width", Config.computerTermWidth, 1, 255);
computerTermHeight = builder.defineInRange("height", Config.computerTermHeight, 1, 255);
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.DEFAULT_COMPUTER_TERM_WIDTH, 1, 255);
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.DEFAULT_COMPUTER_TERM_HEIGHT, 1, 255);
builder.pop();
builder.comment("Terminal size of pocket computers.").push("pocket_computer");
pocketTermWidth = builder.defineInRange("width", Config.pocketTermWidth, 1, 255);
pocketTermHeight = builder.defineInRange("height", Config.pocketTermHeight, 1, 255);
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.DEFAULT_POCKET_TERM_WIDTH, 1, 255);
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.DEFAULT_POCKET_TERM_HEIGHT, 1, 255);
builder.pop();
builder.comment("Maximum size of monitors (in blocks).").push("monitor");
monitorWidth = builder.defineInRange("width", Config.monitorWidth, 1, 32);
monitorHeight = builder.defineInRange("height", Config.monitorHeight, 1, 32);
monitorWidth = builder.comment("Maximum width of monitors").defineInRange("width", Config.monitorWidth, 1, 32);
monitorHeight = builder.comment("Maximum height of monitors").defineInRange("height", Config.monitorHeight, 1, 32);
builder.pop();
builder.pop();
@@ -437,10 +437,6 @@ public final class ConfigSpec {
Config.turtlesCanPush = turtlesCanPush.get();
// Terminal size
Config.computerTermWidth = computerTermWidth.get();
Config.computerTermHeight = computerTermHeight.get();
Config.pocketTermWidth = pocketTermWidth.get();
Config.pocketTermHeight = pocketTermHeight.get();
Config.monitorWidth = monitorWidth.get();
Config.monitorHeight = monitorHeight.get();
}

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.shared.lectern;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.stats.Stats;
@@ -14,6 +15,7 @@ import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.context.UseOnContext;
@@ -21,8 +23,12 @@ import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import org.jetbrains.annotations.Nullable;
/**
* Extends {@link LecternBlock} with support for {@linkplain PrintoutItem printouts}.
@@ -55,6 +61,27 @@ public class CustomLecternBlock extends LecternBlock {
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity be) be.setItem(item.split(1));
}
/**
* A default implementation of {@link Item#useOn(UseOnContext)} for items that can be placed on a lectern.
*
* @param context The context of this item usage action.
* @return Whether the item was placed or not.
*/
public static InteractionResult defaultUseItemOn(UseOnContext context) {
var level = context.getLevel();
var blockPos = context.getClickedPos();
var blockState = level.getBlockState(blockPos);
if (blockState.is(Blocks.LECTERN) && !blockState.getValue(LecternBlock.HAS_BOOK)) {
// If we have an empty lectern, place our book into it.
if (!level.isClientSide) {
CustomLecternBlock.replaceLectern(level, blockPos, blockState, context.getItemInHand());
}
return InteractionResult.sidedSuccess(level.isClientSide);
} else {
return InteractionResult.PASS;
}
}
/**
* Remove a custom lectern and replace it with an empty vanilla one.
*
@@ -131,7 +158,7 @@ public class CustomLecternBlock extends LecternBlock {
clearLectern(level, pos, state);
} else {
// Otherwise open the screen.
player.openMenu(lectern);
lectern.openMenu(player);
}
player.awardStat(Stats.INTERACT_WITH_LECTERN);
@@ -139,4 +166,11 @@ public class CustomLecternBlock extends LecternBlock {
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
return level.isClientSide ? null : BlockEntityHelpers.createTickerHelper(type, ModRegistry.BlockEntities.LECTERN.get(), serverTicker);
}
private static final BlockEntityTicker<CustomLecternBlockEntity> serverTicker = (level, pos, state, lectern) -> lectern.tick();
}

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