1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-16 14:37:39 +00:00

Compare commits

...

79 Commits

Author SHA1 Message Date
Jonathan Coates
fbf994e803 Merge branch 'mc-1.20.x' into mc-1.21.x 2025-06-28 11:21:22 +01:00
Jonathan Coates
8344c0a5c2 Bump CC:T to 1.116.0 2025-06-28 11:03:52 +01:00
Jonathan Coates
a292d33830 Syntax highlighting for multiline tokens in edit
I don't love the implementation of this (see discussion in #2220), but
it's better than nothing. Wow, the editor really needs a bit of a
rewrite, the code is kinda messy.

Fixes #1396.
2025-06-25 22:50:23 +01:00
Jonathan Coates
341d1c7bc2 Move paint/edit menu bar into common module
I want to add a menu bar to the edit runner too, so let's make this code
a little more reusable first.
2025-06-25 22:14:09 +01:00
Jonathan Coates
531eacfac7 Merge pull request #2224 from matematikaadit/patch-1
Miniscule typo fix in the shell.path() doc comment
2025-06-21 16:59:01 +01:00
Adit Cahya Ramadhan
1f3da5205c Miniscule typo fix in the shell.path() doc comment
Noticed this when reading the shell API page in the wiki.
2025-06-21 22:29:04 +07:00
Jonathan Coates
798ceefafe Add test for crafting of disks
See #2221.
2025-06-18 21:00:32 +01:00
Jonathan Coates
7c0f79fc3c Move edit_runner into its own module
I've a few more features I'd like to add to it. Moving it out makes it
slightly easier to maintain.
2025-06-17 17:55:24 +01:00
Jonathan Coates
b35cefc5dd Switch from path to parentPath
I've been putting this off for a while, as I had issues in the past with
people using old Node versions (e.g. #1806), but it no long works on my
machine, so time to make the switch.

Also do a bit of a package update. Hit a rollup bug while doing this
(https://github.com/rollup/plugins/issues/1877), so holding that update
back for now.
2025-06-17 17:46:10 +01:00
Jonathan Coates
89dd521930 Merge branch 'mc-1.20.x' into mc-1.21.x 2025-06-15 16:31:51 +01:00
Jonathan Coates
9272e2efcd Use vanilla's nine-slice sprites for border rendering
- Move remaining sprites to the vanilla GUI atlas.

 - Convert our computer border/sidebar sprites to use vanilla's
   nine-sliced mcmeta files. I thought I'd have to do something custom
   here for the sidebar, as that has no right border, but vanilla
   supports that natively!

 - Use the normal GuiGraphics.blitSprite for rendering computer
   border/sidebar.

 - Obey nine-slice scaling within the pocket computer renderer.
2025-06-15 16:25:54 +01:00
Jonathan Coates
69353a4fcf Use lexer for edit's syntax highlighting
This is slightly more accurate for long strings and comments. Note that
we still work a line at a time (and in a non-incremental manner), so
doesn't actaully support multi-line strings (#1396).

We do now treat goto as a keyword (fixes #1653). We don't currently
support labels — those *technically* aren't a token (`:: foo --[[ a
comment ]] ::` is a valid label!), but maybe we could special-case the
short `::foo::` form.
2025-06-15 13:25:21 +01:00
Jonathan Coates
ff363dca5a Move sidebar_advanced.png down by one pixel
Apparently this has been broken since the file was created in
53546b9f57d9acaa4cdca14ae00eaf68ce8c50bd!? I'm sure I fixed this before,
but maybe that was a different but similar issue >_>.
2025-06-14 18:44:16 +01:00
LorneHyde
1c51282426 Fix syntax highlighting for strings ending in an escaped backslash (#2194) 2025-06-08 19:55:14 +00:00
Jonathan Coates
4a3a1c9275 Merge pull request #2214 from Wojbie/patch-1
Update motd path in startup.lua
2025-06-03 08:08:30 +01:00
Wojbie
2557dd0af9 Update motd path in startup.lua
Removes situations where shell resolution caused arbitrary program called `motd` at root get executed instead of expected /rom one.
2025-06-03 01:03:02 +02:00
Jonathan Coates
b5c0c6e104 Fix out-of-bounds when pasting too-long text
Used a `<=` instead of a `<`! How did I mess this up!?

Fixes #2209
2025-06-02 08:58:43 +01:00
SpartanSoftware
876fd8ddb8 Fix 0 being treated as a valid colour (#2211) 2025-05-31 07:46:03 +00:00
Jonathan Coates
ee3b1343b5 Handle keyboard layouts for our computer shortcuts
Convert GLFW's key codes back to their actual key, and then use that
when checking keyboard shortcuts. We *don't* do this for the paste key,
just to be consistent with vanilla's behaviour.

Fixes #2207.
2025-05-25 22:32:39 +01:00
JackMacWindows
b440b964b7 Add notes about minor changed file handle behavior in 1.109.0 (#2203) 2025-05-25 20:24:26 +00:00
Jonathan Coates
5dfc401b45 Update build plugin versions 2025-05-18 10:05:27 +01:00
Jonathan Coates
418c9be7ac Allow changing terminal size with components
We add a new computercraft:term_size component that allows changing the
terminal size of computers and pocket computers.
2025-05-16 18:22:20 +01:00
Jonathan Coates
b491f6b11f Merge branch 'mc-1.20.x' into mc-1.21.x 2025-05-16 17:56:51 +01:00
Jonathan Coates
4344c3072f Add a test for turtle upgrade crafting
Every few years I get confused about which side turtle upgrades go on
when crafting. The fact that it's flipped always throws me! Let's add a
comment to the recipe, and add some tests to reassure myself.
2025-05-10 14:39:51 +01:00
Jonathan Coates
947001104d Update Cobalt to 0.9.6
- Allow heterogenous __lt/__le.
2025-04-21 14:36:44 +01:00
Jonathan Coates
8711512769 Remove allocation tracking for computers
Reverts 76968f2f28. We'd originally added
this to gather some numbers for #1580, with the hope that it would also
be useful for server admins. Sadly, it's not as accurate as I originally
hoped — the number sometimes goes down for unclear reasons (something to
do with the TLAB maybe??).

Closes #1739.
2025-04-21 08:32:03 +01:00
Jonathan Coates
fdae94b3c1 Merge branch 'mc-1.20.x' into mc-1.21.x 2025-03-25 08:44:27 +00:00
Jonathan Coates
9c0ce27ce6 Switch a few more places to use Java 17 features
New ErrorProne hint, and one which is actually pretty useful!
2025-03-22 09:39:47 +00:00
Jonathan Coates
c458360b18 Bump versions of build tooling
The main thing of note is Spotless, which also bumps the version of
Ktlint. I've been putting this off for a while[^1], as this changed a
bunch of formatting, and Spotless's (broken) caching was making it hard
to test. Ended up downloading ktlint and running it localy.

[^1]: 8204944b5f
2025-03-21 14:28:31 +00:00
Jonathan Coates
09ad6c1905 Add tags for disks and floppies
Fixes #2158
2025-03-20 19:16:16 +00:00
Jonathan Coates
0e1e8a72d3 Fix syncing of colour between PocketBrain and item
- Actually set colour when constructing the brain.
 - Sync it back after crafting, much like we do for upgrades (see
   dcc74e15c7) for more details.

We should take a proper look at this on 1.21.4 and make these methods
main-thread only, so we can sync immediately.

Fixes #2157
2025-03-20 18:54:06 +00:00
Jonathan Coates
ffa6eadc26 Register our BEs as entities
Fixes #2141. Hah, I wrote some tests for this in
b03546a158, but they pass because hoppers
still support vanilla inventories, but turtles don't.

Wish NeoForge registered a fallback for any inventory, like Fabric does,
but there we go.
2025-03-16 16:37:29 +00:00
Jonathan Coates
7c1e8e1951 Fix usages of javax's Nullable annotation 2025-03-16 16:29:19 +00:00
Jonathan Coates
b805a34c2d Merge branch 'mc-1.20.x' into mc-1.21.x 2025-03-16 16:28:53 +00:00
Jonathan Coates
b03546a158 Add game tests to check our blocks are inventories
See #2141
2025-03-16 15:59:43 +00:00
Jonathan Coates
582713467f Remove air blocks from test structures
This is a bit nasty, but makes the structure files *significtly* smaller
(1/4 the size), so feels worth doing.
2025-03-16 15:59:43 +00:00
Jonathan Coates
b6f41a0df5 Fix several issues with char/paste event validation
- Fix isValidClipboard always returning true.
 - Fix characters >=128 being rejected. We changed the signature from a
   byte to an int in 0f123b5efd, but
   didn't update all call sites.

   Valhalla cannot come soon enough. I would love to be able to have
   (cheap) wrapper classes for some of these types.

See Zeus-guy's comments in #860.
2025-03-16 14:07:15 +00:00
Jonathan Coates
594738a022 Standardised item details docs a little
Sort of closes #2125. I've really struggled to find a way to make it
clear that the information returned here is a snapshot of the current
item, and not a live view and/or proxy. Most wordings I've tried end up
feeling really clunky — given that this is a relatively rare
misunderstanding, let's not stress about this too much.
2025-03-16 10:25:57 +00:00
Jonathan Coates
27f2ab364c Clean up disk <-> drive right clicking
Oh dear. I'd originally set out to *remove* logic from DiskItem — we're
so close to being able to remove this item in 1.21! However, while
looking at this code, I realised I could remove the whole Forge-specific
doesSneakBypassUse.

We now remove the use hook on the block, and override useOn on the item.
Obvious in retrospect!
2025-03-15 12:28:29 +00:00
Jonathan Coates
5a43273757 docs: specify valid types for settings.define (#2140) 2025-03-13 07:10:30 +00:00
Drew Edwards
97e28516fb docs: specify valid types for settings.define 2025-03-13 01:40:08 +00:00
Jonathan Coates
676fb5fb53 Remove usages of onNeighborChange
Oh. This is from ye olde days (it's one of the first PRs to CC[^1]!). In
pre-1.13 days, furnaces changing their lit state would replace the block
(creating a new BE) and then set back the old BE. CC wouldn't pick up
the second event, and so would continue to use the peripheral from the
first.

We don't really need this any more, for a couple of reasons:
 a) Furnaces don't do this any more.

 b) Peripherals are now refreshed on the next tick rather than
    immediately.

 c) Forge's capabilities have an explicit invalidate() hook already. This
    technically only detects *removing* block entities, but I'm not sure
    there's any cases where you add a block entity without also
    triggering a block state change.

Ironically, the place we probably need this more is Fabric, where the
lookup API doesn't have a public invalidate hook (it's hidden away in
the BlockApiCache). I'm mostly relying on c) here, in that we just won't
see this happen in practice.

[^1]: https://github.com/dan200/ComputerCraft/pull/180
2025-03-09 15:22:49 +00:00
Jonathan Coates
08dc08b5a3 Replace appendHoverText with component-based tooltips
We have several items (e.g. ComputerItem), which only exist for their
custom tooltip implementation. We remove these, and replace them
vanilla-style component-based tooltips (TooltipProvider).

The implementation here is a little janky — as the vanilla list of
components is hard-coded, and neither mod loader offers a way to extend
it. For now we just use the generic mod-loader tooltip hook — this
probably would be easier with a mixin, but let's do things Properly.

It would be nice to fully remove DiskItem (we only keep this around for
doesSneakBypassUse), but that can be a future task.
2025-03-09 13:39:45 +00:00
Jonathan Coates
8f4d4038f6 Merge branch 'mc-1.20.x' into mc-1.21.x 2025-03-09 12:35:44 +00:00
Jonathan Coates
63ba3fe274 Fix printout crafting
Introduced by the previous commit — I'd made one of the checks too lax.
Add some tests for this, so it doesn't happen again, though this code
does get a complete rewrite in 1.21 anyway >_>.
2025-03-09 12:19:59 +00:00
Jonathan Coates
749b3df227 Remove PrintoutItem.getType
Kinda surprised this is still around! Not sure why I kept it post
the-flattening really, it's been redundant for a while.
2025-03-09 11:46:36 +00:00
Jonathan Coates
b97634b717 Flesh out LuaTable a bit
Add a whole buncha helper methods for parsing values, much like
IArguments. This allows us to remove TableHelper. Gosh, that dates back
to 2018!
2025-03-08 23:39:11 +00:00
Jonathan Coates
1b8344d0a3 Ignore shader loading errors
Another go at fixing #2127.

In a892739f8e we set the precision on the
Tbo uniform. However, this is stripped in the shader pre-processing
Pojav/gl4es does, and so has no effect. As a (terrible) workaround, we
now just ignore shader loading errors. This probably does leak memory
(we'll never clean up the program), but there's not much we can do about
that.
2025-03-05 19:01:32 +00:00
Jonathan Coates
b42bc0a01a Bump Loom and vanilla-extract versions 2025-03-05 18:49:03 +00:00
Jonathan Coates
70a7478529 Ignore some components when sending item to client
We send the item-form of the current computer in the computer menu data.
However, this leaks the current LockCode, as we include all components.
We now only gather a safe subset of components when constructing the
item.
2025-03-05 18:20:05 +00:00
Jonathan Coates
0cff73e2fc Add turtle.getEquipped{Left,Right}
These just return details about the currently equipped *item*. This
allows us to expose information about the currently equipped upgrade,
without having to invent a whole new format.

Docs are a bit consise, but didn't really know how to flesh them out any
further.

Fixes #964, fixes #1613, closes #1692.
2025-03-03 21:30:19 +00:00
Jonathan Coates
a892739f8e Specify precision in monitor fragment shader
Some people run Minecraft on OpenGL ES GPUs via the gl4es translation
bridge. This sets the default precision for floats and ints, but not
usamplerBuffer.

Using lowp should be fine here (we don't need to encode much info!), but
we use mediump just in case. Have run this through the Mali Offline
compiler, and it seems fine with it.

Fixes #2127.
2025-03-03 10:13:26 +00:00
Jonathan Coates
f8785a092f Fix particle texture for turtle colour model
This is never actually used in practice, but let's avoid any missing
texture reference warnings! Embarrassing that I hadn't noticed this
before!
2025-03-02 21:14:27 +00:00
Jonathan Coates
598fc4aefd Merge branch 'mc-1.20.x' into mc-1.21.x 2025-03-01 22:49:56 +00:00
Jonathan Coates
dd7e8fcefc Bump CC:T to 1.115.1 2025-03-01 22:35:29 +00:00
Jonathan Coates
29c8f96912 Sync translations from CrowdIn
I remembered to do this *before* the update! 🎉
2025-03-01 22:34:45 +00:00
Jonathan Coates
b9267ecbfc Resize lectern pocket computer textures
This bumps them to be 48x48, which allows them to be downscaled to a
mipmap level of 4. We possibly should bump these to be 64x64 (actual
power of two), but I kinda want to avoid that, as it's so much wasted
space. If this does become a problem, we should probably put these on a
separate atlas instead.
2025-03-01 22:24:27 +00:00
Jonathan Coates
9d2c2db22b Fix speaker.playAudio not updating volume
Honestly, the whole design around volume and playSound/playAudio is a
little janky — it probably should be a separate setVolume method which
updates directly. But too late to change that now, so let's do what we
can.

See #2108
2025-03-01 20:28:37 +00:00
ellellie
6660966320 Update Create dependency to 6.0.0 (#2117) 2025-03-01 19:56:43 +00:00
Jonathan Coates
3acb231f01 Write some docs for migrating from MC 1.20 -> 1.21
Almost definitely too late (and not sure if anyone will read them
anyway), but we can but try.
2025-02-21 20:12:44 +00:00
Jonathan Coates
16324e1eac Flesh out detail provider/registry docs 2025-02-19 21:08:56 +00:00
Jonathan Coates
fa33949113 Merge AbstractComputerItem and ComputerItem
ComputerItem was just an empty subclass, so no sense keeping around. We
probably could get rid of CommandComputerItem too (just do an `instanceof
GameMasterBlock` in `getPlacementState`), but there's no rush.
2025-02-16 21:12:35 +00:00
Jonathan Coates
0c04d9de47 Merge branch 'mc-1.20.x' into mc-1.21.x 2025-02-16 21:04:28 +00:00
Jonathan Coates
32f5c38485 Remove IMedia implementations from our items
We now register these separately, rather than relying on the implicit
IMedia. This allows us to share a bit more logic between
PocketComputerItem and AbstractComputerItem. This doesn't make much
difference on 1.20.1, but does help a bit more on 1.21.1.
2025-02-16 20:32:01 +00:00
Jonathan Coates
01fe949b3e Use Fabric's Item Lookup for registering media providers
We'll switch to capabilities on the (Neo)Forge side for 1.21, when the
capability system is less painful, and then fully deprecate in 1.21.4.
2025-02-16 20:32:01 +00:00
Jonathan Coates
c03fce275e Add some abstractions for registering capabilities
This is pretty useless right now (not even going to try to use it for
Forge), but should be a bit more useful for 1.21.
2025-02-16 20:32:00 +00:00
Jonathan Coates
0998acaa82 Switch to JSpecify annotations
Now, hear me out, what if instead of having three @Nullable annotations,
we had *four*?

I've been wanting to switch away from javax.annoations for a while. The
library has been deprecated for ever and, unlike other @Nullable
annotations, the annotation is attached to the parameter/function
itself, rather than the type.

We use JSpecify rather than one of the alternatives (JetBrains,
CheckerFramework) mostly because it's what NullAway recommends. We keep
CheckerFramework around for @DefaultQualifier, and JB's for @Contract.

There are some ugly changes here — for instance, `@Nullable byte[]` is
replace by `byte @Nullable`, and `@Nullable ILuaMachine.Factory` is
`ILuaMachine.@Nullable Factory`. Ughr, I understand why, but it does not
spark joy :).
2025-02-16 18:09:15 +00:00
Jonathan Coates
12a44fed6f Sync translations from CrowdIn 2025-02-15 18:35:05 +00:00
Jonathan Coates
3f8c3b026a Merge branch 'mc-1.20.x' into mc-1.21.x 2025-02-14 20:44:39 +00:00
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
1278246cf7 Add back MoreRed support
I removed this in fc834cd97f, way back in
late 2024. Looks like it's been updating in the meantime and I hadn't
noticed, so add it back.

I've simplified the code a little bit, to make use of our NeoForge's new
capability system, but otherwise it's almost exactly the same :D.
2025-02-12 13:40:58 +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
1e25fa9bc3 Bump CC:T to 1.114.5 2025-02-09 10:16:45 +00:00
Jonathan Coates
74f707aaea Create a new CraftingInput for each craft
It's not actually safe to reuse this, as we need to recompute the
internal StackedContents each time the inventory changes, otherwise
ShapelessRecipe.matches will continue to return true, even if the actual
inventory doesn't include the required items.

Fixes #2094
2025-02-07 09:45:14 +00:00
572 changed files with 5903 additions and 11987 deletions

View File

@@ -27,7 +27,7 @@ indent_size = 2
[*.yml]
indent_size = 2
[{*.kt,*.kts}]
[*.{kt,kts}]
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_size = 4
ij_kotlin_spaces_around_equality_operators = true
@@ -39,3 +39,14 @@ ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_method_parameters_wrap = off
ij_kotlin_call_parameters_wrap = off
ij_kotlin_extends_list_wrap = off
ktlint_code_style = intellij_idea
ktlint_standard_class-naming = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_function-naming = disabled
ktlint_standard_no-wildcard-imports = disabled
# FIXME: These two are disable right now as they're over-eager in putting things
# on the same line. We should set max_line_length and handle this properly.
ktlint_standard_function-signature = disabled
ktlint_standard_function-expression-body = disabled

View File

@@ -6,7 +6,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer

View File

@@ -88,8 +88,8 @@ You'll first need to [set up a development environment as above](#setting-up-a-d
Once this is set up, you can now run `./gradlew docWebsite`. This generates documentation from our Lua and Java code,
writing the resulting HTML into `./projects/web/build/site`, which can then be opened in a browser. When iterating on
documentation, you can instead run `./gradlew docWebsite -t`, which will rebuild documentation every time you change a
file.
documentation, you can instead run `./gradlew :web:assemble -x :web:compileTeaVM -t`, which will rebuild documentation
every time you change a file.
Documentation is built using [illuaminate] which, while not currently documented (somewhat ironic), is largely the same
as [ldoc][ldoc]. Documentation comments are written in Markdown, though note that we do not support many GitHub-specific

View File

@@ -6,7 +6,6 @@
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {

View File

@@ -29,7 +29,7 @@ base.archivesName.convention("cc-tweaked-$mcVersion-${project.name}")
java {
toolchain {
languageVersion= CCTweakedPlugin.JAVA_VERSION
languageVersion = CCTweakedPlugin.JAVA_VERSION
}
withSourcesJar()
@@ -48,7 +48,7 @@ repositories {
includeGroup("cc.tweaked")
// Things we mirror
includeGroup("com.simibubi.create")
includeGroup("commoble.morered")
includeGroup("net.commoble.morered")
includeGroup("dev.architectury")
includeGroup("dev.emi")
includeGroup("maven.modrinth")
@@ -91,9 +91,9 @@ sourceSets.all {
options.errorprone {
check("InvalidBlockTag", CheckSeverity.OFF) // Broken by @cc.xyz
check("InvalidParam", CheckSeverity.OFF) // Broken by records.
check("InlineMeSuggester", CheckSeverity.OFF) // Minecraft uses @Deprecated liberally
// Too many false positives right now. Maybe we need an indirection for it later on.
check("AssignmentExpression", CheckSeverity.OFF) // I'm a bad person.
check("ReferenceEquality", CheckSeverity.OFF)
check("EnumOrdinal", CheckSeverity.OFF) // For now. We could replace most of these with EnumMap.
check("OperatorPrecedence", CheckSeverity.OFF) // For now.
@@ -122,7 +122,6 @@ tasks.compileTestJava {
}
}
tasks.withType(JavaCompile::class.java).configureEach {
options.encoding = "UTF-8"
}
@@ -171,7 +170,7 @@ tasks.test {
tasks.withType(JacocoReport::class.java).configureEach {
reports.xml.required = true
reports.html.required =true
reports.html.required = true
}
project.plugins.withType(CCTweakedPlugin::class.java) {
@@ -195,30 +194,23 @@ spotless {
fun FormatExtension.defaults() {
endWithNewline()
trimTrailingWhitespace()
indentWithSpaces(4)
leadingTabsToSpaces(4)
}
java {
defaults()
importOrder("", "javax|java", "\\#")
removeUnusedImports()
}
val ktlintConfig = mapOf(
"ktlint_standard_no-wildcard-imports" to "disabled",
"ktlint_standard_class-naming" to "disabled",
"ktlint_standard_function-naming" to "disabled",
"ij_kotlin_allow_trailing_comma" to "true",
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
)
kotlinGradle {
defaults()
ktlint().editorConfigOverride(ktlintConfig)
ktlint()
}
kotlin {
defaults()
ktlint().editorConfigOverride(ktlintConfig)
ktlint()
}
}

View File

@@ -5,10 +5,8 @@
package cc.tweaked.gradle
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.AbstractExecTask
import org.gradle.api.tasks.OutputDirectory
import java.io.File
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
@get:OutputDirectory

View File

@@ -25,7 +25,6 @@ import javax.xml.xpath.XPathFactory
* Would be good to PR some (or all) of these changes upstream at some point.
*
* @see net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask
* @see net.minecraftforge.gradle.common.util.runs.IntellijRunGenerator
*/
internal class IdeaRunConfigurations(project: Project) {
private val rootProject = project.rootProject
@@ -35,22 +34,6 @@ internal class IdeaRunConfigurations(project: Project) {
private val writer = TransformerFactory.newInstance().newTransformer()
private val ideaDir = rootProject.file(".idea/")
private val buildDir: Lazy<String?> = lazy {
val ideaMisc = ideaDir.resolve("misc.xml")
try {
val doc = Files.newBufferedReader(ideaMisc.toPath()).use {
documentBuilder.parse(InputSource(it))
}
val node =
xpath.evaluate("//component[@name=\"ProjectRootManager\"]/output", doc, XPathConstants.NODE) as Node
val attr = node.attributes.getNamedItem("url") as Attr
attr.value.removePrefix("file://")
} catch (e: Exception) {
LOGGER.error("Failed to find root directory", e)
null
}
}
fun patch() = synchronized(LOCK) {
val runConfigDir = ideaDir.resolve("runConfigurations")
@@ -58,10 +41,9 @@ internal class IdeaRunConfigurations(project: Project) {
Files.list(runConfigDir.toPath()).use {
for (configuration in it) {
val filename = configuration.fileName.toString();
val filename = configuration.fileName.toString()
when {
filename.endsWith("_fabric.xml") -> patchFabric(configuration)
filename.startsWith("forge_") && filename.endsWith(".xml") -> patchForge(configuration)
else -> {}
}
}
@@ -72,65 +54,6 @@ internal class IdeaRunConfigurations(project: Project) {
setXml("//configuration", "folderName") { "Fabric" }
}
private fun patchForge(path: Path) = withXml(path) {
val configId = path.fileName.toString().removePrefix("forge_").removeSuffix(".xml")
val sourceSet = forgeConfigs[configId]
if (sourceSet == null) {
LOGGER.error("[{}] Cannot map run configuration to a known source set", path)
return@withXml
}
setXml("//configuration", "folderName") { "Forge" }
setXml("//configuration/module", "name") { "${rootProject.name}.forge.$sourceSet" }
if (buildDir.value == null) return@withXml
setXml("//configuration/envs/env[@name=\"MOD_CLASSES\"]", "value") { classpath ->
val classes = classpath!!.split(':')
val newClasses = mutableListOf<String>()
fun appendUnique(x: String) {
if (!newClasses.contains(x)) newClasses.add(x)
}
for (entry in classes) {
if (!entry.contains("/out/")) {
appendUnique(entry)
continue
}
val match = CLASSPATH_ENTRY.matchEntire(entry)
if (match != null) {
val modId = match.groups["modId"]!!.value
val proj = match.groups["proj"]!!.value
var component = match.groups["component"]!!.value
if (component == "production") component = "main"
appendUnique(forgeModEntry(modId, proj, component))
} else {
LOGGER.warn("[{}] Unknown classpath entry {}", path, entry)
appendUnique(entry)
}
}
// Ensure common code is on the classpath
for (proj in listOf("common", "common-api")) {
for (component in listOf("main", "client")) {
appendUnique(forgeModEntry("computercraft", proj, component))
}
}
if (newClasses.any { it.startsWith("cctest%%") }) {
appendUnique(forgeModEntry("cctest", "core", "testFixtures"))
appendUnique(forgeModEntry("cctest", "common", "testFixtures"))
appendUnique(forgeModEntry("cctest", "common", "testMod"))
}
newClasses.joinToString(":")
}
}
private fun forgeModEntry(mod: String, project: String, component: String) =
"$mod%%${buildDir.value}/production/${rootProject.name}.$project.$component"
private fun LocatedDocument.setXml(xpath: String, attribute: String, value: (String?) -> String) {
val node = this@IdeaRunConfigurations.xpath.evaluate(xpath, document, XPathConstants.NODE) as Node?
if (node == null) {
@@ -159,16 +82,5 @@ internal class IdeaRunConfigurations(project: Project) {
companion object {
private val LOGGER = Logging.getLogger(IdeaRunConfigurations::class.java)
private val LOCK = Any()
private val CLASSPATH_ENTRY =
Regex("(?<modId>[a-z]+)%%\\\$PROJECT_DIR\\\$/projects/(?<proj>[a-z-]+)/out/(?<component>\\w+)/(?<type>[a-z]+)\$")
private val forgeConfigs = mapOf(
"runClient" to "client",
"runData" to "main",
"runGameTestServer" to "testMod",
"runServer" to "main",
"runTestClient" to "testMod",
)
}
}

View File

@@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import groovy.util.Node
import groovy.util.NodeList
object XmlUtil {
fun findChild(node: Node, name: String): Node? = when (val child = node.get(name)) {
is Node -> child
is NodeList -> child.singleOrNull() as Node?
else -> null
}
}

View File

@@ -101,7 +101,10 @@ SPDX-License-Identifier: MPL-2.0
<module name="InvalidJavadocPosition" />
<module name="JavadocBlockTagLocation" />
<module name="JavadocMethod"/>
<module name="JavadocType"/>
<module name="JavadocType">
<!-- Seems to complain about @hidden!? -->
<property name="allowUnknownTags" value="true" />
</module>
<module name="JavadocStyle">
<property name="checkHtml" value="false" />
</module>
@@ -152,7 +155,10 @@ SPDX-License-Identifier: MPL-2.0
<module name="NoWhitespaceAfter">
<property name="tokens" value="AT,INC,DEC,UNARY_MINUS,UNARY_PLUS,BNOT,LNOT,DOT,ARRAY_DECLARATOR,INDEX_OP,METHOD_REF" />
</module>
<module name="NoWhitespaceBefore" />
<module name="NoWhitespaceBefore">
<!-- Allow whitespace before "..." for @Nullable annotations -->
<property name="tokens" value="COMMA,SEMI,POST_INC,POST_DEC,LABELED_STAT" />
</module>
<!-- TODO: Decide on an OperatorWrap style. -->
<module name="ParenPad" />
<module name="SeparatorWrap">

View File

@@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false
# Mod properties
isUnstable=true
modVersion=1.114.4
modVersion=1.116.0
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.21.1

View File

@@ -10,7 +10,7 @@
fabric-api = "0.102.1+1.21.1"
fabric-loader = "0.15.11"
neoForge = "21.1.9"
neoForgeSpi = "8.0.1"
neoMergeTool = "2.0.0"
mixin = "0.8.5"
parchment = "2024.07.28"
parchmentMc = "1.21"
@@ -26,12 +26,12 @@ slf4j = "2.0.9"
asm = "9.6"
autoService = "1.1.1"
checkerFramework = "3.42.0"
cobalt = { strictly = "0.9.5" }
cobalt = { strictly = "0.9.6" }
commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2"
jspecify = "1.0.0"
jzlib = "1.1.3"
kotlin = "2.1.0"
kotlin = "2.1.10"
kotlin-coroutines = "1.10.1"
nightConfig = "3.8.1"
@@ -42,12 +42,12 @@ iris-fabric = "1.8.0-beta.3+1.21-fabric"
iris-forge = "1.8.0-beta.3+1.21-neoforge"
jei = "19.8.2.99"
modmenu = "11.0.0-rc.4"
moreRed = "4.0.0.4"
moreRed = "6.0.0.3"
rei = "16.0.729"
sodium-fabric = "mc1.21-0.6.0-beta.1-fabric"
sodium-forge = "mc1.21-0.6.0-beta.1-neoforge"
mixinExtra = "0.3.5"
create-forge = "0.5.1.f-33"
create-forge = "6.0.0-6"
create-fabric = "0.5.1-f-build.1467+mc1.20.1"
# Testing
@@ -58,24 +58,24 @@ junitPlatform = "1.11.4"
jmh = "1.37"
# Build tools
cctJavadoc = "1.8.3"
checkstyle = "10.20.1"
errorProne-core = "2.27.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.9.2"
cctJavadoc = "1.8.4"
checkstyle = "10.23.1"
errorProne-core = "2.38.0"
errorProne-plugin = "4.1.0"
fabric-loom = "1.10.4"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
illuaminate = "0.1.0-74-gf1551d5"
illuaminate = "0.1.0-83-g1131f68"
lwjgl = "3.3.3"
minotaur = "2.8.7"
modDevGradle = "2.0.74"
nullAway = "0.10.25"
modDevGradle = "2.0.95"
nullAway = "0.12.7"
shadow = "8.3.1"
spotless = "6.23.3"
spotless = "7.0.2"
taskTree = "2.1.1"
teavm = "0.11.0-SQUID.1"
vanillaExtract = "0.2.0"
vanillaExtract = "0.2.1"
versionCatalogUpdate = "0.8.1"
[libraries]
@@ -87,10 +87,10 @@ checkerFramework = { module = "org.checkerframework:checker-qual", version.ref =
cobalt = { module = "cc.tweaked:cobalt", version.ref = "cobalt" }
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
neoForgeSpi = { module = "net.neoforged:neoforgespi", version.ref = "neoForgeSpi" }
neoMergeTool = { module = "net.neoforged:mergetool", version.ref = "neoMergeTool" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
jetbrainsAnnotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" }
jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" }
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
jzlib = { module = "com.jcraft:jzlib", version.ref = "jzlib" }
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
kotlin-platform = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
@@ -105,7 +105,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
# Minecraft mods
create-fabric = { module = "com.simibubi.create:create-fabric-1.20.1", version.ref = "create-fabric" }
create-forge = { module = "com.simibubi.create:create-1.20.1", version.ref = "create-forge" }
create-forge = { module = "com.simibubi.create:create-1.21.1", version.ref = "create-forge" }
emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" }
fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" }
fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric-loader" }
@@ -119,7 +119,7 @@ jei-forge = { module = "mezz.jei:jei-1.21-neoforge", version.ref = "jei" }
mixin = { module = "org.spongepowered:mixin", version.ref = "mixin" }
mixinExtra = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinExtra" }
modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" }
moreRed = { module = "commoble.morered:morered-1.20.1", version.ref = "moreRed" }
moreRed = { module = "net.commoble.morered:morered-1.21.1", version.ref = "moreRed" }
rei-api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" }
rei-builtin = { module = "me.shedaniel:RoughlyEnoughItems-default-plugin", version.ref = "rei" }
rei-fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" }
@@ -180,7 +180,7 @@ taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }
[bundles]
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
annotations = ["checkerFramework", "jetbrainsAnnotations", "jspecify"]
kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
# Minecraft

969
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,11 @@
"tslib": "^2.0.3"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-typescript": "^12.0.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.0.0 && <12.1.3",
"@rollup/plugin-url": "^8.0.1",
"@swc/core": "^1.3.92",
"@types/node": "^22.0.0",
"@types/node": "^24.0.0",
"lightningcss": "^1.22.0",
"preact-render-to-string": "^6.2.1",
"rehype": "^13.0.0",

View File

@@ -10,7 +10,7 @@ import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.Nullable;
import java.util.stream.Stream;

View File

@@ -12,8 +12,8 @@ import dan200.computercraft.api.turtle.TurtleSide;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.stream.Stream;
/**

View File

@@ -13,8 +13,7 @@ import dan200.computercraft.impl.client.ClientPlatformHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.core.component.DataComponentPatch;
import org.joml.Matrix4f;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
final class TurtleUpgradeModellers {
private static final Transformation leftTransform = getMatrixFor(-0.4065f);

View File

@@ -12,8 +12,7 @@ import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.ApiStatus;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
@ApiStatus.Internal
public interface ClientPlatformHelper {

View File

@@ -26,8 +26,7 @@ import net.minecraft.core.Direction;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* The static entry point to the ComputerCraft API.
@@ -148,7 +147,10 @@ public final class ComputerCraftAPI {
*
* @param provider The media provider to register.
* @see MediaProvider
* @deprecated Prefer {@code dan200.computercraft.api.media.MediaLookup} (Fabric) or
* {@code dan200.computercraft.api.media.MediaCapability} (NeoForge).
*/
@Deprecated
public static void registerMediaProvider(MediaProvider provider) {
getInstance().registerMediaProvider(provider);
}

View File

@@ -28,6 +28,20 @@ public class ComputerCraftTags {
public static final TagKey<Item> WIRED_MODEM = make("wired_modem");
public static final TagKey<Item> MONITOR = make("monitor");
/**
* Floppy disks. Both the read/write version, and treasure disks.
*
* @since 1.116.0
*/
public static final TagKey<Item> DISKS = make("disks");
/**
* All pocket computers.
*
* @since 1.116.0
*/
public static final TagKey<Item> POCKET_COMPUTERS = make("pocket_computers");
/**
* Items which can be {@linkplain Item#use(Level, Player, InteractionHand) used} when calling
* {@code turtle.place()}.

View File

@@ -6,8 +6,8 @@ package dan200.computercraft.api.detail;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

View File

@@ -8,8 +8,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* A reference to a block in the world, used by block detail providers.

View File

@@ -7,8 +7,8 @@ package dan200.computercraft.api.detail;
import net.minecraft.core.component.DataComponentHolder;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@@ -51,10 +51,10 @@ public abstract class ComponentDetailProvider<T> implements DetailProvider<DataC
* This method is always called on the server thread, so it is safe to interact with the world here, but you should
* take care to avoid long blocking operations as this will stall the server and other computers.
*
* @param data The full details to be returned for this item stack. New properties should be added to this map.
* @param item The component to provide details for.
* @param data The full details to be returned for this item stack. New properties should be added to this map.
* @param component The component to provide details for.
*/
public abstract void provideComponentDetails(Map<? super String, Object> data, T item);
public abstract void provideComponentDetails(Map<? super String, Object> data, T component);
@Override
public final void provideDetails(Map<? super String, Object> data, DataComponentHolder holder) {

View File

@@ -14,6 +14,7 @@ import java.util.Map;
*
* @param <T> The type of object that this provider can provide details for.
* @see DetailRegistry
* @see dan200.computercraft.api.detail An overview of the detail system.
*/
@FunctionalInterface
public interface DetailProvider<T> {

View File

@@ -17,6 +17,7 @@ import java.util.Map;
* also in this package.
*
* @param <T> The type of object that this registry provides details for.
* @see dan200.computercraft.api.detail An overview of the detail system.
*/
@ApiStatus.NonExtendable
public interface DetailRegistry<T> {

View File

@@ -17,6 +17,9 @@ public class VanillaDetailRegistries {
* <p>
* This instance's {@link DetailRegistry#getBasicDetails(Object)} is thread safe (assuming the stack is immutable)
* and may be called from the computer thread.
* <p>
* This does not have special handling for {@linkplain ItemStack#isEmpty() empty item stacks}, and so the returned
* details will be an empty stack of air. Callers should generally check for empty stacks before calling this.
*/
public static final DetailRegistry<ItemStack> ITEM_STACK = ComputerCraftAPIService.get().getItemStackDetailRegistry();

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
/**
* The detail system provides a standard way for mods to return descriptions of common game objects, such as blocks or
* items, as well as registering additional detail to be included in those descriptions.
* <p>
* For instance, the built-in {@code turtle.getItemDetail()} method uses
* {@linkplain dan200.computercraft.api.detail.VanillaDetailRegistries#ITEM_STACK in order to provide information about}
* the selected item:
*
* <pre class="language language-lua">{@code
* local item = turtle.getItemDetail(nil, true)
* --[[
* item = {
* name = "minecraft:wheat",
* displayName = "Wheat",
* count = 1,
* maxCount = 64,
* tags = {},
* }
* ]]
* }</pre>
*
* <h2>Built-in detail providers</h2>
* While you can define your own detail providers (perhaps for types from your own mod), CC comes with several built-in
* detail registries for vanilla and mod-loader objects:
*
* <ul>
* <li>{@link dan200.computercraft.api.detail.VanillaDetailRegistries}, for vanilla objects</li>
* <li>{@code dan200.computercraft.api.detail.ForgeDetailRegistries} for Forge-specific objects</li>
* <li>{@code dan200.computercraft.api.detail.FabricDetailRegistries} for Fabric-specific objects</li>
* </ul>
*
* <h2>Example: Returning details from methods</h2>
* Here we define a {@code getHeldItem()} method for pocket computers which finds the currently held item of the player
* and returns it to the user using {@link dan200.computercraft.api.detail.VanillaDetailRegistries#ITEM_STACK} and
* {@link dan200.computercraft.api.detail.DetailRegistry#getDetails(java.lang.Object)}.
*
* {@snippet class=com.example.examplemod.ExamplePocketPeripheral region=details}
*
* <h2>Example: Registering custom detail registries</h2>
* Here we define a new detail provider for items that includes the nutrition and saturation values in the returned object.
*
* {@snippet class=com.example.examplemod.ExampleMod region=details}
*/
package dan200.computercraft.api.detail;

View File

@@ -9,8 +9,7 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import org.jetbrains.annotations.ApiStatus;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* An interface passed to {@link ILuaAPIFactory} in order to provide additional information

View File

@@ -5,8 +5,7 @@
package dan200.computercraft.api.lua;
import dan200.computercraft.api.ComputerCraftAPI;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Construct an {@link ILuaAPI} for a computer.

View File

@@ -14,14 +14,14 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.JukeboxSong;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Represents an item that can be placed in a disk drive and used by a Computer.
* <p>
* Implement this interface on your {@link Item} class to allow it to be used in the drive. Alternatively, register
* a {@link MediaProvider}.
* Implement this interface on your {@link Item} class to allow it to be used in the drive, or register via
* {@code dan200.computercraft.api.media.MediaLookup} (Fabric) or {@code dan200.computercraft.api.media.MediaCapability}
* (NeoForge).
*/
public interface IMedia {
/**

View File

@@ -5,8 +5,7 @@
package dan200.computercraft.api.media;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* This interface is used to provide {@link IMedia} implementations for {@link ItemStack}.

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 org.jspecify.annotations.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

@@ -12,8 +12,7 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.ApiStatus;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Wrapper class for pocket computers.

View File

@@ -13,8 +13,7 @@ import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* A peripheral which can be equipped to the back side of a pocket computer.

View File

@@ -17,8 +17,7 @@ import net.minecraft.world.Container;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.ApiStatus;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* The interface passed to turtle by turtles, providing methods that they can call.

View File

@@ -16,8 +16,8 @@ import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.function.Function;
/**

View File

@@ -5,8 +5,7 @@
package dan200.computercraft.api.turtle;
import net.minecraft.core.Direction;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Used to indicate the result of executing a turtle command.
@@ -60,9 +59,9 @@ public final class TurtleCommandResult {
private final boolean success;
private final @Nullable String errorMessage;
private final @Nullable Object[] results;
private final @Nullable Object @Nullable [] results;
private TurtleCommandResult(boolean success, @Nullable String errorMessage, @Nullable Object[] results) {
private TurtleCommandResult(boolean success, @Nullable String errorMessage, @Nullable Object @Nullable [] results) {
this.success = success;
this.errorMessage = errorMessage;
this.results = results;
@@ -92,8 +91,7 @@ public final class TurtleCommandResult {
*
* @return The command's result, or {@code null} if it was a failure.
*/
@Nullable
public Object[] getResults() {
public @Nullable Object @Nullable [] getResults() {
return results;
}
}

View File

@@ -18,8 +18,8 @@ import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.Optional;
/**

View File

@@ -13,6 +13,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;
@@ -30,8 +31,7 @@ import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.ApiStatus;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Backing interface for {@link ComputerCraftAPI}
@@ -84,6 +84,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

@@ -5,8 +5,8 @@
package dan200.computercraft.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.io.Serial;
/**

View File

@@ -5,8 +5,8 @@
package dan200.computercraft.impl;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.ServiceLoader;
import java.util.stream.Collectors;

View File

@@ -64,5 +64,65 @@ dependencies {
CC:T), please <a href="https://github.com/cc-tweaked/CC-Tweaked/discussions/new/choose">start a discussion</a> to
let me know!
<h1>Updating from Minecraft 1.20.1 to 1.21.1</h1>
<h2>Peripherals</h2>
<ul>
<li>
<p>
On NeoForge, the peripheral capability has migrated to NeoForge's new capability system.
<code>dan200.computercraft.api.peripheral.PeripheralCapability</code> can be used to register a peripheral.
<code>IPeripheralProvider</code> has also been removed, as capabilities can now be used for arbitrary
blocks.
</ul>
<p>
{@linkplain dan200.computercraft.api.peripheral Read more on registering peripherals}.
<h2>Turtle and pocket upgrades</h2>
Turtle and pocket upgrades have been migrated to use Minecraft's dynamic registries. While upgrades themselves have not
changed much, the interface for registering them is dramatically different.
<ul>
<li>
<p>
<code>TurtleUpgradeSerialiser</code> and <code>PocketUpgradeSerialiser</code> have been unified into a
single {@link dan200.computercraft.api.upgrades.UpgradeType} class
<ul>
<li>
Replace <code>TurtleUpgradeSerialiser.registryId()</code> with
{@link dan200.computercraft.api.turtle.ITurtleUpgrade#typeRegistry()} and <code>PocketUpgradeSerialiser.registryId()</code>
with {@link dan200.computercraft.api.pocket.IPocketUpgrade#typeRegistry()}.
<li>
Replace all other usages of <code>TurtleUpgradeSerialiser</code> and <code>PocketUpgradeSerialiser</code>
with {@link dan200.computercraft.api.upgrades.UpgradeType}.
</ul>
<li>
Upgrades are now (de)serialised using codecs, rather than manually reading from JSON and encoding/decoding
network packets. Instead of subclassing {@link dan200.computercraft.api.upgrades.UpgradeType}, it is recommended
you use {@link dan200.computercraft.api.upgrades.UpgradeType#create} to create a new type from a
<code>MapCodec</code>.
<li>
Upgrades are no longer aware of their ID, and so cannot compute their adjective. The adjective must now either
be hard-coded, or read as part of the codec.
<li>
The upgrade data providers have been removed, in favour of mod-loaders built-in support for dynamic registries.
I'm afraid it's probably easier if you delete your existing upgrade datagen code and start from scratch. See
<a href="./dan200/computercraft/api/turtle/ITurtleUpgrade.html#datagen">the <code>ITurtleUpgrade</code>
documentation for an example</a>.
<li>
Upgrades now store their additional data ({@link dan200.computercraft.api.turtle.ITurtleAccess#getUpgradeData},
{@link dan200.computercraft.api.pocket.IPocketAccess#getUpgradeData()}) as an immutable component map, rather
than a compound tag.
</ul>
<p>
{@linkplain dan200.computercraft.api.turtle.ITurtleUpgrade Read more on registering turtle upgrades}.
</body>
</html>

View File

@@ -39,7 +39,6 @@ dependencies {
compileOnly(libs.mixin)
compileOnly(libs.mixinExtra)
compileOnly(libs.bundles.externalMods.common)
compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false }
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
annotationProcessorEverywhere(libs.autoService)

View File

@@ -38,8 +38,8 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.function.Consumer;
/**

View File

@@ -25,7 +25,6 @@ import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
@@ -41,7 +40,6 @@ import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceProvider;
import net.minecraft.util.FastColor;
import net.minecraft.world.entity.LivingEntity;
@@ -51,8 +49,10 @@ import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.ItemLike;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
@@ -69,6 +69,8 @@ import java.util.function.Supplier;
* @see ModRegistry The common registry for actual game objects.
*/
public final class ClientRegistry {
private static final Logger LOG = LoggerFactory.getLogger(ClientRegistry.class);
private ClientRegistry() {
}
@@ -143,10 +145,6 @@ public final class ClientRegistry {
void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
}
public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
register.accept(GuiSprites.initialise(minecraft.getTextureManager()));
}
private static final ResourceLocation[] EXTRA_MODELS = {
TurtleOverlay.ELF_MODEL,
TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
@@ -160,7 +158,7 @@ public final class ClientRegistry {
public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
register.accept(
(stack, layer) -> layer == 1 ? DiskItem.getColour(stack) : -1,
(stack, layer) -> layer == 1 ? DyedItemColor.getOrDefault(stack, Colour.WHITE.getARGB()) : -1,
ModRegistry.Items.DISK.get()
);
@@ -192,7 +190,18 @@ public final class ClientRegistry {
}
public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
RenderTypes.registerShaders(resources, load);
RenderTypes.registerShaders(resources, (name, create, onLoaded) -> {
ShaderInstance shader;
try {
shader = create.get();
} catch (Exception e) {
LOG.error("Failed to load {}", name, e);
onLoaded.accept(null);
return;
}
load.accept(shader, onLoaded);
});
}
private record UnclampedPropertyFunction(

View File

@@ -15,8 +15,8 @@ import net.minecraft.client.gui.components.ChatComponent;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;
import org.apache.commons.lang3.StringUtils;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.Objects;
/**

View File

@@ -26,11 +26,11 @@ import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;

View File

@@ -6,9 +6,7 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
@@ -40,14 +38,15 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra
public void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
// Draw a border around the terminal
var terminal = getTerminal();
var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES);
var computerTextures = GuiSprites.getComputerTextures(family);
ComputerBorderRenderer.render(
spriteRenderer, computerTextures,
terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
graphics.blitSprite(
computerTextures.border(),
terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2
);
graphics.blitSprite(
Nullability.assertNonNull(computerTextures.sidebar()),
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
);
ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
graphics.flush(); // Flush to ensure background textures are drawn before foreground.
}
}

View File

@@ -7,22 +7,16 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.resources.TextureAtlasHolder;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.stream.Stream;
/**
* Sprite sheet for all GUI texutres in the mod.
*/
public final class GuiSprites extends TextureAtlasHolder {
public static final ResourceLocation SPRITE_SHEET = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui");
public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png");
public final class GuiSprites {
public static final ButtonTextures TURNED_OFF = button("turned_off");
public static final ButtonTextures TURNED_ON = button("turned_on");
public static final ButtonTextures TERMINATE = button("terminate");
@@ -32,6 +26,9 @@ 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);
private GuiSprites() {
}
private static ButtonTextures button(String name) {
return new ButtonTextures(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name),
@@ -47,34 +44,6 @@ public final class GuiSprites extends TextureAtlasHolder {
);
}
private static @Nullable GuiSprites instance;
private GuiSprites(TextureManager textureManager) {
super(textureManager, TEXTURE, SPRITE_SHEET);
}
/**
* Initialise the singleton {@link GuiSprites} instance.
*
* @param textureManager The current texture manager.
* @return The singleton {@link GuiSprites} instance, to register as resource reload listener.
*/
public static GuiSprites initialise(TextureManager textureManager) {
if (instance != null) throw new IllegalStateException("GuiSprites has already been initialised");
return instance = new GuiSprites(textureManager);
}
/**
* Lookup a texture on the atlas.
*
* @param texture The texture to find.
* @return The sprite on the atlas.
*/
public static TextureAtlasSprite get(ResourceLocation texture) {
if (instance == null) throw new IllegalStateException("GuiSprites has not been initialised");
return instance.getSprite(texture);
}
/**
* Get the appropriate textures to use for a particular computer family.
*

View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.gui;
import org.lwjgl.glfw.GLFW;
/**
* Supports for converting/translating key codes.
*/
public class KeyConverter {
/**
* GLFW's key events refer to the physical key code, rather than the "actual" key code (with keyboard layout
* applied).
* <p>
* This makes sense for WASD-style input, but is a right pain for keyboard shortcuts this function attempts to
* translate those keys back to their "actual" key code. See also
* <a href="https://github.com/glfw/glfw/issues/1502"> this discussion on GLFW's GitHub.</a>
*
* @param key The current key code.
* @param scanCode The current scan code.
* @return The translated key code.
*/
public static int physicalToActual(int key, int scanCode) {
var name = GLFW.glfwGetKeyName(key, scanCode);
if (name == null || name.length() != 1) return key;
// If we've got a single character as the key name, treat that as the ASCII value of the key,
// and map that back to a key code.
var character = name.charAt(0);
// 0-9 and A-Z map directly to their GLFW key (they're the same ASCII code).
if ((character >= '0' && character <= '9') || (character >= 'A' && character <= 'Z')) return character;
// a-z map to GLFW_KEY_{A,Z}
if (character >= 'a' && character <= 'z') return GLFW.GLFW_KEY_A + (character - 'a');
return key;
}
}

View File

@@ -15,9 +15,9 @@ import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import org.jspecify.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import javax.annotation.Nullable;
import java.util.Objects;
import static dan200.computercraft.core.util.Nullability.assertNonNull;

View File

@@ -12,8 +12,8 @@ import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.List;
import static dan200.computercraft.core.util.Nullability.assertNonNull;

View File

@@ -127,6 +127,7 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
// Skip rendering labels.
}
@SuppressWarnings("ArrayRecordComponent")
record PrintoutInfo(int pages, boolean book, TextBuffer[] text, TextBuffer[] colour) {
public static final PrintoutInfo DEFAULT = of(PrintoutData.EMPTY, false);

View File

@@ -7,8 +7,7 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
@@ -64,8 +63,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
}
// Render sidebar
var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES);
ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset);
graphics.flush(); // Flush to ensure background textures are drawn before foreground.
graphics.blitSprite(
Nullability.assertNonNull(GuiSprites.getComputerTextures(family).sidebar()),
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
);
}
}

View File

@@ -6,9 +6,7 @@ package dan200.computercraft.client.gui.widgets;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage;
import dan200.computercraft.client.render.SpriteRenderer;
import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.network.chat.Component;
@@ -24,12 +22,9 @@ public final class ComputerSidebar {
private static final int ICON_MARGIN = 2;
private static final int CORNERS_BORDER = 3;
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
private static final int BUTTONS = 2;
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
private static final int TEX_HEIGHT = 14;
public static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
private ComputerSidebar() {
}
@@ -63,14 +58,6 @@ public final class ComputerSidebar {
));
}
public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) {
var texture = textures.sidebar();
if (texture == null) throw new NullPointerException(textures + " has no sidebar texture");
var sprite = GuiSprites.get(texture);
renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT);
}
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {
if (isOn.getAsBoolean()) {
input.shutdown();

View File

@@ -12,8 +12,8 @@ import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.function.Supplier;
/**

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.client.gui.widgets;
import dan200.computercraft.client.gui.KeyConverter;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
@@ -70,7 +71,7 @@ public class TerminalWidget extends AbstractWidget {
@Override
public boolean charTyped(char ch, int modifiers) {
var terminalChar = StringUtil.unicodeToTerminal(ch);
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped(terminalChar);
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
return true;
}
@@ -83,7 +84,7 @@ public class TerminalWidget extends AbstractWidget {
}
if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) {
switch (key) {
switch (KeyConverter.physicalToActual(key, scancode)) {
case GLFW.GLFW_KEY_T -> {
if (terminateTimer < 0) terminateTimer = 0;
}
@@ -119,7 +120,7 @@ public class TerminalWidget extends AbstractWidget {
computer.keyUp(key);
}
switch (key) {
switch (KeyConverter.physicalToActual(key, scancode)) {
case GLFW.GLFW_KEY_T -> terminateTimer = -1;
case GLFW.GLFW_KEY_R -> rebootTimer = -1;
case GLFW.GLFW_KEY_S -> shutdownTimer = -1;

View File

@@ -8,7 +8,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.integration.RecipeModHelpers;
import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import mezz.jei.api.IModPlugin;
@@ -23,6 +22,7 @@ import net.minecraft.client.Minecraft;
import net.minecraft.core.RegistryAccess;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import java.util.List;
@@ -100,7 +100,7 @@ public class JEIComputerCraft implements IModPlugin {
/**
* Distinguishes disks by colour.
*/
private static final IIngredientSubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DiskItem.getColour(stack));
private static final IIngredientSubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DyedItemColor.getOrDefault(stack, -1));
private static RegistryAccess getRegistryAccess() {
return Minecraft.getInstance().level.registryAccess();

View File

@@ -0,0 +1,90 @@
// 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 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.world.inventory.InventoryMenu;
import net.minecraft.world.item.component.DyedItemColor;
/**
* A model for {@linkplain PocketComputerItem pocket computers} placed on a lectern.
*
* @see CustomLecternRenderer
*/
public class LecternPocketModel {
public static final ResourceLocation TEXTURE_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_normal");
public static final ResourceLocation TEXTURE_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_advanced");
public static final ResourceLocation TEXTURE_COLOUR = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_colour");
public static final ResourceLocation TEXTURE_FRAME = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
public static final ResourceLocation TEXTURE_LIGHT = ResourceLocation.fromNamespaceAndPath(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 = 48 / 2;
private static final int TEXTURE_HEIGHT = 48 / 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);
}
/**
* 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 DyedItemColor 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);
root.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);
}
root.render(poseStack, MATERIAL_LIGHT.buffer(bufferSource, RenderType::entityCutout), LightTexture.FULL_BRIGHT, packedOverlay, lightColour);
}
}

View File

@@ -11,8 +11,8 @@ import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.Direction;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

View File

@@ -23,8 +23,8 @@ import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

View File

@@ -27,8 +27,8 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.JukeboxSong;
import net.minecraft.world.level.Level;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.UUID;
/**

View File

@@ -7,8 +7,7 @@ package dan200.computercraft.client.platform;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.model.BakedModel;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
static ClientPlatformHelper get() {
@@ -25,5 +24,5 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
* @param overlayLight The current overlay light.
* @param tints Block colour tints to apply to the model.
*/
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints);
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, int @Nullable [] tints);
}

View File

@@ -10,8 +10,8 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

View File

@@ -8,8 +8,7 @@ import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Clientside data about a pocket computer.

View File

@@ -1,17 +1,14 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: LicenseRef-CCPL
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render;
import dan200.computercraft.client.gui.GuiSprites;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import static dan200.computercraft.client.render.SpriteRenderer.u;
import static dan200.computercraft.client.render.SpriteRenderer.v;
import dan200.computercraft.client.gui.ComputerScreen;
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
/**
* Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or
* Constants for the borders of computers, either for a {@linkplain ComputerScreen GUI} or
* {@linkplain PocketItemRenderer in-hand pocket computers}.
*/
public final class ComputerBorderRenderer {
@@ -21,55 +18,13 @@ public final class ComputerBorderRenderer {
public static final int MARGIN = 2;
/**
* The width of the terminal border.
* The size of the terminal border.
* <p>
* This is only used for layout of elements within UI. When rendering, the size of the computer's border is
* determined by its {@link GuiSpriteScaling}.
*/
public static final int BORDER = 12;
public static final int LIGHT_HEIGHT = 8;
private static final int TEX_SIZE = 36;
private ComputerBorderRenderer() {
}
public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) {
var endX = x + width;
var endY = y + height;
var border = GuiSprites.get(textures.border());
// Top bar
blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER);
blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER);
blitBorder(renderer, border, endX, y - BORDER, BORDER * 2, 0, BORDER, BORDER);
// Vertical bars
blitBorder(renderer, border, x - BORDER, y, 0, BORDER, BORDER, height);
blitBorder(renderer, border, endX, y, BORDER * 2, BORDER, BORDER, height);
// Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the
// pocket computer's lights).
if (withLight) {
var pocketBottomTexture = textures.pocketBottom();
if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
var pocketBottom = GuiSprites.get(pocketBottomTexture);
renderer.blitHorizontalSliced(
pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT,
BORDER, BORDER, BORDER * 3
);
} else {
blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER);
blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER);
blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER);
}
}
private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) {
renderer.blit(
x, y, width, height,
u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE),
u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE)
);
}
}

View File

@@ -6,16 +6,31 @@ 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.ModRegistry;
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
import dan200.computercraft.shared.media.items.PrintoutData;
import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
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.util.FastColor;
import net.minecraft.world.item.component.DyedItemColor;
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}.
@@ -23,10 +38,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
@@ -38,15 +60,53 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
poseStack.translate(0, -0.125f, 0);
var item = lectern.getItem();
if (item.getItem() instanceof PrintoutItem printout) {
if (item.getItem() instanceof PrintoutItem) {
var vertexConsumer = LecternPrintoutModel.MATERIAL.buffer(buffer, RenderType::entitySolid);
if (printout.getType() == PrintoutItem.Type.BOOK) {
if (item.is(ModRegistry.Items.PRINTED_BOOK.get())) {
printoutModel.renderBook(poseStack, vertexConsumer, packedLight, packedOverlay);
} else {
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutData.getOrEmpty(item).pages());
}
} else if (item.getItem() instanceof PocketComputerItem pocket) {
var computer = ClientPocketComputers.get(item);
pocketModel.render(
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), DyedItemColor.getOrDefault(item, -1),
FastColor.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

@@ -14,8 +14,8 @@ import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.world.item.ItemStack;
import org.joml.Vector4f;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.List;
/**
@@ -39,7 +39,7 @@ public final class ModelRenderer {
* @param overlayLight The current overlay light.
* @param tints Block colour tints to apply to the model.
*/
public static void renderQuads(PoseStack transform, VertexConsumer buffer, List<BakedQuad> quads, int lightmapCoord, int overlayLight, @Nullable int[] tints) {
public static void renderQuads(PoseStack transform, VertexConsumer buffer, List<BakedQuad> quads, int lightmapCoord, int overlayLight, int @Nullable [] tints) {
var matrix = transform.last();
var inverted = matrix.pose().determinant() < 0;

View File

@@ -13,13 +13,16 @@ import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
import net.minecraft.util.FastColor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import org.joml.Matrix4f;
import static dan200.computercraft.client.render.ComputerBorderRenderer.*;
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER;
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;
@@ -29,6 +32,11 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON
public final class PocketItemRenderer extends ItemMapLikeRenderer {
public static final PocketItemRenderer INSTANCE = new PocketItemRenderer();
/**
* The height of the pocket computer's light.
*/
private static final int LIGHT_HEIGHT = 8;
private PocketItemRenderer() {
}
@@ -83,14 +91,69 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
}
private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
var textures = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
var spriteRenderer = new SpriteRenderer(transform, render, 0, light, colour);
renderBorder(spriteRenderer, textures, width, height);
}
var r = (colour >>> 16) & 0xFF;
var g = (colour >>> 8) & 0xFF;
var b = colour & 0xFF;
private static void renderBorder(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int width, int height) {
var sprites = Minecraft.getInstance().getGuiSprites();
var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b);
ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true);
// Find our border, forcing it to be a nine-sliced texture.
var borderSprite = sprites.getSprite(textures.border());
var borderSlice = getSlice(sprites.getSpriteScaling(borderSprite), DEFAULT_BORDER);
var borderBounds = borderSlice.border();
// And take the separate bottom bit of the pocket computer.
var bottomTexture = textures.pocketBottom();
if (bottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
var bottomSprite = sprites.getSprite(bottomTexture);
var bottomSlice = getSlice(sprites.getSpriteScaling(bottomSprite), DEFAULT_BOTTOM);
var bottomBounds = bottomSlice.border();
// Now draw a nine-sliced texture, by stitching together the top parts of the border with the pocket bottom.
// Top bar
renderer.blit(
borderSprite, -borderBounds.left(), -borderBounds.top(), borderBounds.left(), borderBounds.top(),
0, 0, borderSlice.width(), borderSlice.height()
);
renderer.blitTiled(
borderSprite, 0, -borderBounds.top(), width, borderBounds.top(),
borderBounds.left(), 0, borderSlice.width() - borderBounds.left() - borderBounds.right(), borderBounds.top(),
borderSlice.width(), borderSlice.height()
);
renderer.blit(
borderSprite, width, -borderBounds.top(), borderBounds.right(), borderBounds.top(),
borderSlice.width() - borderBounds.right(), 0, borderSlice.width(), borderSlice.height()
);
// Vertical bars
renderer.blitTiled(
borderSprite, -borderBounds.left(), 0, borderBounds.left(), height,
0, borderBounds.top(), borderBounds.left(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(),
borderSlice.width(), borderSlice.height()
);
renderer.blitTiled(
borderSprite, width, 0, borderBounds.right(), height,
borderSlice.width() - borderBounds.right(), borderBounds.top(), borderBounds.right(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(),
borderSlice.width(), borderSlice.height()
);
// Bottom
renderer.blit(
bottomSprite, -bottomBounds.left(), height, bottomBounds.left(), bottomSlice.height(),
0, 0, bottomSlice.width(), bottomSlice.height()
);
renderer.blitTiled(
bottomSprite, 0, height, width, bottomSlice.height(),
bottomBounds.left(), 0, bottomSlice.width() - bottomBounds.left() - bottomBounds.right(), bottomSlice.height(),
bottomSlice.width(), bottomSlice.height()
);
renderer.blit(
bottomSprite, width, height, bottomBounds.right(), bottomSlice.height(),
bottomSlice.width() - bottomBounds.right(), 0, bottomSlice.width(), bottomSlice.height()
);
}
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
@@ -101,4 +164,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP
);
}
private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice(
36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12)
);
private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice(
36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0)
);
private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) {
return scaling instanceof GuiSpriteScaling.NineSlice slice ? slice : fallback;
}
}

View File

@@ -8,7 +8,6 @@ import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.items.PrintoutData;
import dan200.computercraft.shared.media.items.PrintoutItem;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.decoration.ItemFrame;
@@ -53,7 +52,7 @@ public final class PrintoutItemRenderer extends ItemMapLikeRenderer {
var pageData = stack.getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
var pages = pageData.pages();
var book = ((PrintoutItem) stack.getItem()).getType() == PrintoutItem.Type.BOOK;
var book = stack.is(ModRegistry.Items.PRINTED_BOOK.get());
double width = LINE_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2;
double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2;

View File

@@ -7,7 +7,6 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import net.minecraft.client.renderer.GameRenderer;
@@ -16,11 +15,11 @@ import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceProvider;
import org.apache.commons.io.function.IOSupplier;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -54,11 +53,6 @@ public class RenderTypes {
*/
public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
/**
* Render type for {@linkplain GuiSprites GUI sprites}.
*/
public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE);
public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
return monitorTboShader;
@@ -68,9 +62,12 @@ public class RenderTypes {
return Objects.requireNonNull(GameRenderer.getRendertypeTextShader(), "Text shader has not been registered");
}
public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
load.accept(
new MonitorTextureBufferShader(
public interface ShaderLoader {
void tryLoad(String name, IOSupplier<ShaderInstance> create, Consumer<@Nullable ShaderInstance> accept) throws IOException;
}
public static void registerShaders(ResourceProvider resources, ShaderLoader load) throws IOException {
load.tryLoad("monitor shader", () -> new MonitorTextureBufferShader(
resources,
ComputerCraftAPI.MOD_ID + "/monitor_tbo",
MONITOR_TBO.format()

View File

@@ -6,129 +6,69 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.resources.ResourceLocation;
import org.joml.Matrix4f;
/**
* A {@link GuiGraphics}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity
* renderer).
* A {@link GuiGraphics}-equivalent renders to a {@link VertexConsumer}. This is suitable for rendering outside of a
* GUI, such as part of an entity renderer.
* <p>
* This batches all render calls together, though requires that all {@link TextureAtlasSprite}s are on the same sprite
* sheet.
*/
public class SpriteRenderer {
public static final ResourceLocation TEXTURE = ResourceLocation.withDefaultNamespace("textures/atlas/gui.png");
private final Matrix4f transform;
private final VertexConsumer builder;
private final MultiBufferSource buffers;
private final int light;
private final int z;
private final int r, g, b;
private final int colour;
public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) {
public SpriteRenderer(Matrix4f transform, MultiBufferSource buffers, int z, int light, int colour) {
this.transform = transform;
this.builder = builder;
this.buffers = buffers;
this.z = z;
this.light = light;
this.r = r;
this.g = g;
this.b = b;
this.colour = colour;
}
public static SpriteRenderer createForGui(GuiGraphics graphics, RenderType renderType) {
return new SpriteRenderer(
graphics.pose().last().pose(), graphics.bufferSource().getBuffer(renderType),
0, RenderTypes.FULL_BRIGHT_LIGHTMAP, 255, 255, 255
);
public void blit(TextureAtlasSprite sprite, int x0, int y0, int width, int height, int spriteX, int spriteY, int spriteWidth, int spriteHeight) {
if (width == 0 || height == 0) return;
var x1 = x0 + width;
var y1 = y0 + height;
var u0 = sprite.getU((float) spriteX / spriteWidth);
var u1 = sprite.getU((float) (spriteX + width) / spriteWidth);
var v0 = sprite.getV((float) spriteY / spriteHeight);
var v1 = sprite.getV((float) (spriteY + height) / spriteHeight);
var vertices = buffers.getBuffer(RenderType.text(sprite.atlasLocation()));
vertices.addVertex(transform, x0, y1, z).setColor(colour).setUv(u0, v1).setLight(light);
vertices.addVertex(transform, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
vertices.addVertex(transform, x1, y0, z).setColor(colour).setUv(u1, v0).setLight(light);
vertices.addVertex(transform, x0, y0, z).setColor(colour).setUv(u0, v0).setLight(light);
}
/**
* Render a single sprite.
*
* @param sprite The texture to draw.
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
*/
public void blit(TextureAtlasSprite sprite, int x, int y, int width, int height) {
blit(x, y, width, height, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1());
}
public void blitTiled(
TextureAtlasSprite sprite,
int x, int y, int width, int height,
int tileX, int tileY, int tileWidth, int tileHeight, int spriteWidth, int spriteHeight
) {
if (width <= 0 || height <= 0) return;
if (tileWidth <= 0 || tileHeight <= 0) {
throw new IllegalArgumentException("Tiled sprite texture size must be positive, got " + tileWidth + "x" + tileHeight);
}
/**
* Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiGraphics#blitNineSliced},
* the middle texture is stretched rather than repeated.
*
* @param sprite The texture to draw.
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
* @param leftBorder The width of the left border.
* @param rightBorder The width of the right border.
* @param textureWidth The width of the whole texture.
*/
public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) {
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders");
var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth);
var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth);
blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1());
blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1());
blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1());
}
/**
* Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiGraphics#blitNineSliced},
* the middle texture is stretched rather than repeated.
*
* @param sprite The texture to draw.
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
* @param topBorder The height of the top border.
* @param bottomBorder The height of the bottom border.
* @param textureHeight The height of the whole texture.
*/
public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) {
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders");
var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight);
var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight);
blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart);
blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd);
blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1());
}
/**
* The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite.
*
* @param x The x position of the rectangle we'll draw.
* @param y The x position of the rectangle we'll draw.
* @param width The width of the rectangle we'll draw.
* @param height The height of the rectangle we'll draw.
* @param u0 The first U coordinate.
* @param v0 The first V coordinate.
* @param u1 The second U coordinate.
* @param v1 The second V coordinate.
*/
public void blit(
int x, int y, int width, int height, float u0, float v0, float u1, float v1) {
builder.addVertex(transform, x, y + height, z).setColor(r, g, b, 255).setUv(u0, v1).setLight(light);
builder.addVertex(transform, x + width, y + height, z).setColor(r, g, b, 255).setUv(u1, v1).setLight(light);
builder.addVertex(transform, x + width, y, z).setColor(r, g, b, 255).setUv(u1, v0).setLight(light);
builder.addVertex(transform, x, y, z).setColor(r, g, b, 255).setUv(u0, v0).setLight(light);
}
public static float u(TextureAtlasSprite sprite, int x, int width) {
return sprite.getU((float) x / width);
}
public static float v(TextureAtlasSprite sprite, int y, int height) {
return sprite.getV((float) y / height);
for (var xOffset = 0; xOffset < width; xOffset += tileWidth) {
var sliceWidth = Math.min(tileWidth, width - xOffset);
for (var yOffset = 0; yOffset < height; yOffset += tileHeight) {
var sliceHeight = Math.min(tileHeight, height - yOffset);
blit(sprite, x + xOffset, y + yOffset, sliceWidth, sliceHeight, tileX, tileY, spriteWidth, spriteHeight);
}
}
}
}

View File

@@ -26,8 +26,7 @@ import net.minecraft.util.CommonColors;
import net.minecraft.util.FastColor;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
public static final ResourceLocation COLOUR_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
@@ -63,8 +62,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
var matrix = transform.last().pose();
var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
var width = -font.width(label) / 2.0f;
font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
font.drawInBatch(label, width, (float) 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
font.drawInBatch(label, width, 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
font.drawInBatch(label, width, 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
transform.popPose();
}
@@ -124,7 +123,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.popPose();
}
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, @Nullable int[] tints) {
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, int @Nullable [] tints) {
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getModelManager();
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
}
@@ -140,7 +139,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
* @param tints Tints for the quads, as an array of RGB values.
* @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel
*/
private void renderModel(PoseStack transform, MultiBufferSource renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) {
private void renderModel(PoseStack transform, MultiBufferSource renderer, int lightmapCoord, int overlayLight, BakedModel model, int @Nullable [] tints) {
ClientPlatformHelper.get().renderBakedModel(transform, renderer, model, lightmapCoord, overlayLight, tints);
}

View File

@@ -31,12 +31,12 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.world.phys.AABB;
import org.joml.Matrix4f;
import org.jspecify.annotations.Nullable;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL31;
import org.lwjgl.system.MemoryUtil;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.function.Consumer;

View File

@@ -11,12 +11,12 @@ import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraft.core.BlockPos;
import org.jspecify.annotations.Nullable;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.Set;

View File

@@ -14,12 +14,12 @@ import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.server.packs.resources.ResourceProvider;
import org.jspecify.annotations.Nullable;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL31;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;

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

@@ -10,9 +10,9 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.sounds.AudioStream;
import net.minecraft.client.sounds.SoundEngine;
import org.jspecify.annotations.Nullable;
import org.lwjgl.BufferUtils;
import javax.annotation.Nullable;
import javax.sound.sampled.AudioFormat;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

View File

@@ -10,8 +10,7 @@ import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
@@ -25,7 +24,7 @@ public class SpeakerInstance {
SpeakerInstance() {
}
private void pushAudio(EncodedAudio buffer) {
private void pushAudio(EncodedAudio buffer, float volume) {
var sound = this.sound;
var stream = currentStream;
@@ -33,18 +32,30 @@ public class SpeakerInstance {
var exhausted = stream.isEmpty();
stream.push(buffer);
// If we've got nothing left in the buffer, enqueue an additional one just in case.
if (exhausted && sound != null && sound.stream == stream && stream.channel != null && stream.executor != null) {
if (sound == null) return;
var volumeChanged = sound.setVolume(volume);
if ((exhausted || volumeChanged) && sound.stream == stream && stream.channel != null && stream.executor != null) {
var actualStream = sound.stream;
stream.executor.execute(() -> {
var channel = Nullability.assertNonNull(actualStream.channel);
if (!channel.stopped()) channel.pumpBuffers(1);
if (channel.stopped()) return;
// If we've got nothing left in the buffer, enqueue an additional one just in case.
if (exhausted) channel.pumpBuffers(1);
// Update the attenuation if the volume has changed: SoundEngine.tickNonPaused updates the volume
// itself, but leaves the attenuation unchanged. We mirror the logic of SoundEngine.play here.
if (volumeChanged) {
channel.linearAttenuation(Math.max(volume, 1) * sound.getSound().getAttenuationDistance());
}
});
}
}
public void playAudio(SpeakerPosition position, float volume, EncodedAudio buffer) {
pushAudio(buffer);
pushAudio(buffer, volume);
var soundManager = Minecraft.getInstance().getSoundManager();

View File

@@ -16,8 +16,8 @@ import net.minecraft.client.sounds.SoundBufferLibrary;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity;
import org.jspecify.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
/**
@@ -83,4 +83,10 @@ public class SpeakerSound extends AbstractSoundInstance implements TickableSound
public @Nullable AudioStream getStream() {
return stream;
}
boolean setVolume(float volume) {
if (volume == this.volume) return false;
this.volume = volume;
return true;
}
}

View File

@@ -15,7 +15,7 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
import dan200.computercraft.shared.util.DataComponentUtil;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Nullable;
import org.jspecify.annotations.Nullable;
import java.util.stream.Stream;

View File

@@ -8,6 +8,7 @@ import com.mojang.serialization.Codec;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.data.client.ExtraModelsProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay;
@@ -71,9 +72,11 @@ public final class DataProviders {
out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
UpgradeSlot.LEFT_UPGRADE,
UpgradeSlot.RIGHT_UPGRADE,
LecternPrintoutModel.TEXTURE
LecternPrintoutModel.TEXTURE,
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
)));
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites(
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
@@ -82,6 +85,8 @@ public final class DataProviders {
));
});
generator.add(ResourceMetadataProvider::new);
generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) {
@Override
public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {

View File

@@ -108,6 +108,8 @@ public final class LanguageProvider implements DataProvider {
add(ComputerCraftTags.Items.TURTLE, "Turtles");
add(ComputerCraftTags.Items.WIRED_MODEM, "Wired modems");
add(ComputerCraftTags.Items.MONITOR, "Monitors");
add(ComputerCraftTags.Items.DISKS, "Disks");
add(ComputerCraftTags.Items.POCKET_COMPUTERS, "Pocket Computers");
add(ComputerCraftTags.Items.DYEABLE, "Dyable items");
add(ComputerCraftTags.Items.TURTLE_CAN_PLACE, "Turtle-placeable items");
@@ -187,7 +189,6 @@ public final class LanguageProvider implements DataProvider {
// Metrics
add(Metrics.COMPUTER_TASKS, "Tasks");
add(Metrics.SERVER_TASKS, "Server tasks");
add(Metrics.JAVA_ALLOCATION, "Java Allocations");
add(Metrics.PERIPHERAL_OPS, "Peripheral calls");
add(Metrics.FS_OPS, "Filesystem operations");
add(Metrics.HTTP_REQUESTS, "HTTP requests");

View File

@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dan200.computercraft.client.gui.GuiSprites;
import net.minecraft.client.resources.metadata.gui.GuiMetadataSection;
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.data.metadata.PackMetadataGenerator;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.metadata.MetadataSectionType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
/**
* Similar to {@link PackMetadataGenerator}, but for individual resources.
*/
final class ResourceMetadataProvider implements DataProvider {
private final PackOutput output;
ResourceMetadataProvider(PackOutput output) {
this.output = output;
}
private void register(Builder builder) {
for (var computerTextures : List.of(
GuiSprites.COMPUTER_ADVANCED,
GuiSprites.COMPUTER_COLOUR,
GuiSprites.COMPUTER_COMMAND,
GuiSprites.COMPUTER_NORMAL
)) {
builder.texture(computerTextures.border()).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
new GuiSpriteScaling.NineSlice(36, 36, simpleNineSlicedBorder(12))
));
var sidebar = computerTextures.sidebar();
if (sidebar != null) {
builder.texture(sidebar).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
new GuiSpriteScaling.NineSlice(17, 14, new GuiSpriteScaling.NineSlice.Border(3, 4, 0, 3))
));
}
var pocketBottom = computerTextures.pocketBottom();
if (pocketBottom != null) {
builder.texture(pocketBottom).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0))
));
}
}
}
private static GuiSpriteScaling.NineSlice.Border simpleNineSlicedBorder(int size) {
return new GuiSpriteScaling.NineSlice.Border(size, size, size, size);
}
@Override
public CompletableFuture<?> run(CachedOutput cachedOutput) {
var builder = new Builder();
register(builder);
var outputPath = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK);
return CompletableFuture.allOf(builder.metadata.entrySet().stream().map(entry -> {
var json = new JsonObject();
entry.getValue().elements.forEach((name, element) -> json.add(name, element.get()));
return DataProvider.saveStable(cachedOutput, json, outputPath.resolve(entry.getKey().getNamespace()).resolve(entry.getKey().getPath() + ".mcmeta"));
}).toArray(CompletableFuture[]::new));
}
@Override
public String getName() {
return "Resource Metadata";
}
/**
* A builder for a set of {@code mcmeta} files.
*/
private static final class Builder {
private final Map<ResourceLocation, FileMetadata> metadata = new HashMap<>();
FileMetadata texture(ResourceLocation texture) {
return file(texture.withPrefix("textures/").withSuffix(".png"));
}
FileMetadata file(ResourceLocation path) {
return metadata.computeIfAbsent(path, p -> new FileMetadata());
}
}
/**
* A builder for a given file's {@code mcmeta} file.
*/
private static final class FileMetadata {
private final Map<String, Supplier<JsonElement>> elements = new HashMap<>();
<T> FileMetadata add(MetadataSectionType<T> type, T value) {
elements.put(type.getMetadataSectionName(), () -> type.toJson(value));
return this;
}
}
}

View File

@@ -96,6 +96,8 @@ class TagProvider {
tags.copy(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE);
tags.tag(ComputerCraftTags.Items.WIRED_MODEM).add(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get());
tags.copy(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR);
tags.tag(ComputerCraftTags.Items.DISKS).add(ModRegistry.Items.DISK.get(), ModRegistry.Items.TREASURE_DISK.get());
tags.tag(ComputerCraftTags.Items.POCKET_COMPUTERS).add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
tags.tag(ComputerCraftTags.Items.DYEABLE)
.addTag(ComputerCraftTags.Items.TURTLE)

View File

@@ -6,7 +6,7 @@ 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;
import org.jspecify.annotations.Nullable;
/**
* An example API that will be available on every turtle. This demonstrates both registering an API, and how to write

View File

@@ -3,7 +3,9 @@ package com.example.examplemod;
import com.example.examplemod.data.TurtleUpgradeProvider;
import com.example.examplemod.peripheral.FurnacePeripheral;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.detail.VanillaDetailRegistries;
import dan200.computercraft.api.upgrades.UpgradeType;
import net.minecraft.core.component.DataComponents;
/**
* Our example mod, containing the various things we register.
@@ -34,6 +36,16 @@ public final class ExampleMod {
ComputerCraftAPI.registerGenericSource(new FurnacePeripheral());
// @end region=generic_source
// @start region=details
VanillaDetailRegistries.ITEM_STACK.addProvider((out, stack) -> {
var food = stack.get(DataComponents.FOOD);
if (food == null) return;
out.put("saturation", food.saturation());
out.put("nutrition", food.nutrition());
});
// @end region=details
ExampleAPI.register();
}
}

View File

@@ -0,0 +1,49 @@
package com.example.examplemod;
import dan200.computercraft.api.detail.DetailRegistry;
import dan200.computercraft.api.detail.VanillaDetailRegistries;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.pocket.IPocketAccess;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.LivingEntity;
import org.jspecify.annotations.Nullable;
import java.util.Map;
/**
* An example peripheral for pocket computers. This currently doesn't have an associated upgrade it mostly exists to
* demonstrate other functionality.
*/
public class ExamplePocketPeripheral implements IPeripheral {
private final IPocketAccess pocket;
public ExamplePocketPeripheral(IPocketAccess pocket) {
this.pocket = pocket;
}
@Override
public String getType() {
return "example";
}
/**
* An example of using {@linkplain DetailRegistry detail registries} to get the current player's held item.
*
* @return The item details, or {@code null} if the player is not holding an item.
*/
// @start region=details
@LuaFunction(mainThread = true)
public final @Nullable Map<String, ?> getHeldItem() {
if (!(pocket.getEntity() instanceof LivingEntity entity)) return null;
var heldItem = entity.getItemInHand(InteractionHand.MAIN_HAND);
return heldItem.isEmpty() ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(heldItem);
}
// @end region=details
@Override
public boolean equals(@Nullable IPeripheral other) {
return other instanceof ExamplePocketPeripheral o && pocket == o.pocket;
}
}

View File

@@ -3,7 +3,7 @@ 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;
import org.jspecify.annotations.Nullable;
/**
* A peripheral that adds a {@code getFuel()} method to brewing stands. This demonstrates the usage of

View File

@@ -4,7 +4,7 @@ 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;
import org.jspecify.annotations.Nullable;
/**
* A peripheral that tracks what computers it is attached to.

View File

@@ -205,8 +205,10 @@
"item.computercraft.treasure_disk": "Floppy Disk",
"itemGroup.computercraft": "ComputerCraft",
"tag.item.computercraft.computer": "Computers",
"tag.item.computercraft.disks": "Disks",
"tag.item.computercraft.dyeable": "Dyable items",
"tag.item.computercraft.monitor": "Monitors",
"tag.item.computercraft.pocket_computers": "Pocket Computers",
"tag.item.computercraft.turtle": "Turtles",
"tag.item.computercraft.turtle_can_place": "Turtle-placeable items",
"tag.item.computercraft.wired_modem": "Wired modems",
@@ -217,7 +219,6 @@
"tracking_field.computercraft.http_download.name": "HTTP download",
"tracking_field.computercraft.http_requests.name": "HTTP requests",
"tracking_field.computercraft.http_upload.name": "HTTP upload",
"tracking_field.computercraft.java_allocation.name": "Java Allocations",
"tracking_field.computercraft.max": "%s (max)",
"tracking_field.computercraft.peripheral.name": "Peripheral calls",
"tracking_field.computercraft.server_tasks.name": "Server tasks",

View File

@@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@@ -0,0 +1,10 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": 12,
"height": 36,
"width": 36
}
}
}

View File

@@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 0,
"left": 12,
"right": 12,
"top": 0
},
"height": 20,
"width": 36
}
}
}

View File

@@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 0,
"left": 12,
"right": 12,
"top": 0
},
"height": 20,
"width": 36
}
}
}

View File

@@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 0,
"left": 12,
"right": 12,
"top": 0
},
"height": 20,
"width": 36
}
}
}

View File

@@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 3,
"left": 3,
"right": 0,
"top": 4
},
"height": 14,
"width": 17
}
}
}

View File

@@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 3,
"left": 3,
"right": 0,
"top": 4
},
"height": 14,
"width": 17
}
}
}

View File

@@ -0,0 +1,15 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"border": {
"bottom": 3,
"left": 3,
"right": 0,
"top": 4
},
"height": 14,
"width": 17
}
}
}

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