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

Compare commits

...

84 Commits

Author SHA1 Message Date
Jonathan Coates
e81a2c72ce Merge branch 'mc-1.21.x' into mc-1.21.y 2025-07-12 21:46:28 +01:00
Jonathan Coates
01de6110c6 Re-enable JEI integration 2025-07-12 21:41:50 +01:00
Jonathan Coates
9cf0f85fcb Merge branch 'mc-1.20.x' into mc-1.21.x 2025-07-12 19:00:00 +01:00
Jonathan Coates
018ce7c8a5 Bump CC:T to 1.116.1 2025-07-12 18:57:05 +01:00
Jonathan Coates
44726827b4 Sync translations from Crowdin 2025-07-12 09:49:14 +01:00
Jonathan Coates
2bf0aba455 Don't use SpeakerPosition to get speaker's level
We use getLevel() specifically for reading the current registries
(and/or server). We don't need the exact position of the speaker to
query this, so add a dedicated method for it.

We actually had a similar method on 1.21.7 already for upgrades. This
just moves it to SpeakerPeripheral.

Fixes #2236.
2025-07-10 01:07:51 +01:00
Jonathan Coates
3cf914cb4c Fix NPE when loading mcfunctions
CommandSourceStack.getServer can be null, despite being marked as
non-nullable. Mojang!!! *shakes fist*

Fixes #2235.
2025-07-10 00:47:42 +01:00
Jonathan Coates
4868c4aa32 Small bits of cleanup
- Remove unused MonitorRenderer. I'm sure this had been deleted
   already, but apparently not!
 - Add missing items to the changelog.
 - Fix crash when clearing tests in a world.
 - Bump Iris deps, to help with debugging #2229.
2025-07-10 00:29:06 +01:00
Batári Balázs László
180156ff1c Add lang hu_hu (#2232) 2025-07-06 21:47:22 +00:00
Jonathan Coates
c6ba753568 Update allowed Minecraft versions 2025-07-06 22:34:43 +01:00
Jonathan Coates
9f45c91925 Update to latest TeaVM 2025-07-02 09:21:43 +01:00
Jonathan Coates
67412a2b72 Update to Minecraft 1.21.7 2025-06-30 22:55:55 +01:00
Jonathan Coates
5fa724ed24 Move Create integration setup to its proper place
Merge conflict gone wrong, I assume.
2025-06-30 22:53:15 +01:00
Jonathan Coates
76869593f0 Merge branch 'mc-1.21.x' into mc-1.21.y 2025-06-28 11:31:32 +01:00
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
846f9dff03 Update to latest NeoForge
Welllll, latest when I wrote the code, not at actual time of writing.
2025-06-24 22:59:44 +01:00
Jonathan Coates
64d10ad45b Data fixers for turtle owners 2025-06-24 22:58:04 +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
e3fecb013a Update to Minecraft 1.26.1
Still one TODO left, but around data fixers, so fairly small.

 - GUI rendering got a big overhaul. I avoided the worst of it with
   9272e2efcd, but things like terminals
   and printouts still require some custom rendering.

 - Item models are now de-duplicated when rendering in the UI, so we
   need to keep track of their identity with
   (appendModelidentityElement). I'm not sure I've got this entirely
   right — whole thing feels unfortunately error-prone.

 - BE serialisation now goes through a Value{Input,Output} class, rather
   than using NBT directly. Fairly simple change, but has changed the
   format of the GameProfile used in turtle's owners. Need a DFU patch
   for this.
2025-06-18 21:19:04 +01: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
ec3dd328b3 Construct ByteBuffer manually in monitor renderer
In the 1.21.4 update (9277aa33e9), we
removed our DirectVertexBuffer class, and switched to vanilla's
VertexBuffer. This forced us to use MeshData and thus
ByteBufferBuilder instead of allocating the ByteBuffer ourselves.

One thing I'd missed with this is that Iris's text vertex sink API
requires us to allocate the whole buffer up-front and so the resulting
buffer has a limit of the *maximum* number of vertices rendered, not the
actual one.

This wasn't an issue on 1.21.4, as we didn't check this (I guess we just
silently rendered junk??), but for the 1.21.5 update
(a1df196673) we added some extra
assertions here, which now fail on Iris.

Typically, the whole original change was is now entirely redundant, as
Vanilla has removed VertexBuffer entirely, and so we can/should work in
terms of raw ByteBuffers again.

Fixes #2219.
2025-06-15 18:04:52 +01:00
Jonathan Coates
f3f43191ab Merge branch 'mc-1.21.x' into mc-1.21.y 2025-06-15 16:59:48 +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
0790a8346a Merge branch 'mc-1.21.x' into mc-1.21.y 2025-05-16 18:38:21 +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
acafc06449 Make turtle upgrade models data driven
It's ItemModel, but for upgrades!
2025-05-11 09:17:54 +01:00
Jonathan Coates
598fd98a8b Load turtle overlays during model loading
Back in f10e401aea, we changed turtle
overlays to be loaded as a dynamic registry. This solved some of the
problems we had with upgrades (elf-compatibility), and seemed like a
good idea at the time.

However, because overlays are part of datapacks (not resource packs), we
also needed support for loading overlay models, which we did via an
"extra_models.json" file.

With the ItemModel changes, we can go for a different approach:
 - Turtle overlays are now stored on the item/BE as a simple
   ResourceLocation again.

 - The TurtleOverlay class is moved to the client, and loaded from
   resource packs, during model loading. They're now split into a baked
   form (holding a StandaloneModel) and an unbaked one (holding the
   ResourceLocation).

 - extra_model.json is no longer supported.
2025-05-10 23:30:39 +01:00
Jonathan Coates
e13e8ff92e Fix a couple of rendering issues
- Fix printouts in item frames being rendered too far foward.
 - Fix wired modems being flipped.
2025-05-10 17:52:12 +01:00
Jonathan Coates
0a0c80db41 Allow equipping pocket computers on the bottom
This allows equipping pocket computers on both the back (as before) and
bottom of a pocket computer. The asymmetry is a little unfortunate here,
but makes some sense with the crafting recipe (above goes behind, below
goes on the bottom).

 - Move some functionality from IPocketAccess into a PocketComputer
   interface (and PocketComputerInternal) interface, used by the pocket
   API.

   IPocketAccess preserves the same interface as before. Unlike
   ITurtleAccess, we /don't/ expose the PocketSide in the public API.

 - Several pocket-computer methods (e.g. setUpgradeData, setColour) are
   now required to be called on the main thread, and when the computer
   is being held. This allows us to write back changes to the item
   immediately, rather than the next time the item is ticked.

   Sadly this doesn't actually remove the need for onCraftedPostProcess
   as I'd originally hoped, but I think does make the code a little
   simpler.

 - Rename "computercraft:pocket_computer" component to
   "computercraft:back_pocket_computer".

 - And finally, support multiple upgrades on the pocket computer. This
   is actually quite an easy change, just tedious — there's lots of
   places to update!

Fixes #1406, and I think fixes #1148 — you can use a speaker to notify
you now.
2025-05-10 17:15:41 +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
c20336286b Update to FAPI 0.122
- Use new tooltip component registry. This means we can move the
   tooltip appending magic to Neo only.

 - Use new chunk level change event. I'm not actually sure if we need
   this on recent versions (I can't reproduce the monitor update bug
   that we introduced this to fix), but I've no clue what's changed here.
2025-05-02 19:01:26 +01:00
Jonathan Coates
356366ede8 Undo dev changed to build script 2025-04-30 22:33:22 +01:00
Jonathan Coates
a1df196673 Update to Minecraft 1.21.5
0/10, would not recommend. Though increasingly feeling that about
modding as a whole — really not feeling as emotionally rewarding as it
once did.

Server-side changes are well, not simple, but relatively straightforward:

 - Block removal code is now called before the BE is removed, not after.

   - Monitors now need to track if they've being removed or not again.

   - Turtle drop consuming code no longer tries to insert items into
     the turtle immediately, instead waiting 'til the action is
     complete. Otherwise if the turtle gets destroyed mid-action
     (e.g. the block explodes), then it tries to insert its drops into
     itself!

     We previously guarded against this by checking if the turtle BE had
     been removed, but obviously this no longer works, so just easier to
     shift the insertion.

  - The interface for reading/writing NBT has been overhauled. It has
    native "getOr" and codec support (nice!) but also has been changed
    again in the latest snapshot (less nice!).

 - The dye item component no longer has a "hide tooltip" flag. We now
   hide the tooltip with a default component instead.

 - Related to the above, we can now do all the tooltip-related things we
   needed to do with vanilla's TooltipProvider. This did require
   splitting NonNegativeId into subclasses for disk/computer, but
   otherwise is quite nice.

 - Some changes to model datagen. Annoying, but boring.

 - Game tests got a complete overhaul. I'm keeping the interface the
   same for now (@GameTest), because I'm blowed if I'm datagenning test
   instances :p. If it's any consolation, both NF and Fabric are doing
   this too.

Client changes are a bit more involved though:

 - VertexBuffer has been entirely removed. We now construct the
   GpuBuffer directly.

 - BakedModel is gone! Oh this caused so much suffering for turtle
   models. I ended up rewriting the whole system in processes (which
   then involved PRs to NF and Fabric). Rather than returning a
   TransformedModel, turtle models are now responsible for rendering the
   model.

   This may see another rewrite in the future. I'd like to switch to
   JSON-based turtle models (like item models), but that's blocked on
   some changes to NF right now.

   Sorry to all add-on devs, I know this is a big change.
2025-04-30 22:31:30 +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
a939ad8b97 Merge branch 'mc-1.21.x' into mc-1.21.y 2025-03-25 08:45:03 +00: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
995a6e7379 Merge branch 'mc-1.21.x' into mc-1.21.y 2025-03-18 09:29:35 +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
8ade1c38ac Send less computer BE data to the client
We only sent the id, label and lock code to the client for pick-block
interactions. Now that's handled on the server, we don't need this any
more!
2025-03-05 20:46:06 +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
05163a4911 Store command computers in a separate folder
- Remove the /computercraft-computer-folder client command, and replace
   it with OPEN_FILE on NeoForge, or a /computercraft-open-folder
   command on Fabric (which now accepts a path, rather than an id).

 - Store command computer files in "computercraft/command_computer/<id>".

Fixes #1581.
2025-03-03 20:54:06 +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
499 changed files with 8285 additions and 14688 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

@@ -11,7 +11,8 @@ body:
What version of Minecraft are you using? If your version is not listed, please try to reproduce on one of the supported versions.
options:
- 1.20.1
- 1.21.x
- 1.21.1
- 1.21.7
validations:
required: true
- type: input

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

@@ -68,5 +68,5 @@ tasks.ideaSyncTask {
tasks.named("checkDependencyConsistency", DependencyCheck::class.java) {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
// Minecraft depends on asm, but Fabric forces it to a more recent version
override(libs.findLibrary("asm").get(), "9.7.1")
override(libs.findLibrary("asm").get(), "9.8")
}

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()
@@ -93,6 +93,7 @@ sourceSets.all {
check("InvalidBlockTag", CheckSeverity.OFF) // Broken by @cc.xyz
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.
@@ -121,7 +122,6 @@ tasks.compileTestJava {
}
}
tasks.withType(JavaCompile::class.java).configureEach {
options.encoding = "UTF-8"
}
@@ -170,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) {
@@ -194,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

@@ -12,6 +12,7 @@ files:
de: de_de # German
es-ES: es_es # Spanish
fr: fr_fr # French
hu: hu_hu # Hungarian
it: it_it # Italian
ja: ja_jp # Japanese
ko: ko_kr # Korean

View File

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

View File

@@ -7,26 +7,26 @@
# Minecraft
# MC version is specified in gradle.properties, as we need that in settings.gradle.
# Remember to update corresponding versions in fabric.mod.json/neoforge.mods.toml
fabric-api = "0.118.0+1.21.4"
fabric-loader = "0.16.10"
neoForge = "21.4.101-beta"
neoForgeSpi = "8.0.1"
fabric-api = "0.128.0+1.21.7"
fabric-loader = "0.16.14"
neoForge = "21.7.19-beta"
neoMergeTool = "2.0.0"
mixin = "0.8.5"
parchment = "2024.12.07"
parchmentMc = "1.21.4"
yarn = "1.21.4+build.1"
parchment = "2025.06.29"
parchmentMc = "1.21.6"
yarn = "1.21.7+build.1"
# Core dependencies (these versions are tied to the version Minecraft uses)
fastutil = "8.5.15"
guava = "33.3.1-jre"
netty = "4.1.115.Final"
netty = "4.1.118.Final"
slf4j = "2.0.16"
# Core dependencies (independent of Minecraft)
asm = "9.7.1"
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"
jspecify = "1.0.0"
@@ -38,14 +38,14 @@ nightConfig = "3.8.1"
# Minecraft mods
emi = "1.1.7+1.21"
fabricPermissions = "0.3.3"
iris-fabric = "1.8.8+1.21.4-fabric"
iris-forge = "1.8.8+1.21.4-neoforge"
jei = "19.8.2.99"
iris-fabric = "1.9.1+1.21.7-fabric"
iris-forge = "1.9.1+1.21.7-neoforge"
jei = "23.1.0.4"
modmenu = "13.0.2"
moreRed = "6.0.0.3"
rei = "18.0.800"
sodium-fabric = "mc1.21.4-0.6.10-fabric"
sodium-forge = "mc1.21.4-0.6.10-neoforge"
sodium-fabric = "mc1.21.6-0.6.13-fabric"
sodium-forge = "mc1.21.6-0.6.13-neoforge"
mixinExtra = "0.3.5"
create-forge = "6.0.0-6"
create-fabric = "0.5.1-f-build.1467+mc1.20.1"
@@ -58,24 +58,24 @@ junitPlatform = "1.11.4"
jmh = "1.37"
# Build tools
cctJavadoc = "1.8.3"
checkstyle = "10.21.2"
errorProne-core = "2.36.0"
cctJavadoc = "1.8.5"
checkstyle = "10.23.1"
errorProne-core = "2.38.0"
errorProne-plugin = "4.1.0"
fabric-loom = "1.9.2"
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.12.3"
modDevGradle = "2.0.99"
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"
teavm = "0.13.0-SQUID.1"
vanillaExtract = "0.2.1"
versionCatalogUpdate = "0.8.1"
[libraries]
@@ -87,7 +87,7 @@ 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" }
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
@@ -113,9 +113,9 @@ fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-l
fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" }
iris-fabric = { module = "maven.modrinth:iris", version.ref = "iris-fabric" }
iris-forge = { module = "maven.modrinth:iris", version.ref = "iris-forge" }
jei-api = { module = "mezz.jei:jei-1.21-common-api", version.ref = "jei" }
jei-fabric = { module = "mezz.jei:jei-1.21-fabric", version.ref = "jei" }
jei-forge = { module = "mezz.jei:jei-1.21-neoforge", version.ref = "jei" }
jei-api = { module = "mezz.jei:jei-1.21.7-common-api", version.ref = "jei" }
jei-fabric = { module = "mezz.jei:jei-1.21.7-fabric", version.ref = "jei" }
jei-forge = { module = "mezz.jei:jei-1.21.7-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" }
@@ -186,7 +186,7 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
# Minecraft
externalMods-common = ["iris-forge", "jei-api", "nightConfig-core", "nightConfig-toml"]
externalMods-forge-compile = ["moreRed", "iris-forge", "jei-api"]
externalMods-forge-runtime = []
externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric-compile = ["fabricPermissions", "iris-fabric", "jei-api", "rei-api", "rei-builtin"]
externalMods-fabric-runtime = []

960
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-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.0.0",
"@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

@@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client;
import com.google.common.base.Suppliers;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.item.BlockModelWrapper;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ARGB;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.joml.Vector3f;
import org.jspecify.annotations.Nullable;
import java.util.List;
import java.util.function.Supplier;
/**
* A standalone model.
* <p>
* This is very similar to vanilla's {@link BlockModelWrapper}, but suitable for use in both {@link ItemModel}s and
* block models. This is primarily intended for use with {@link TurtleUpgradeModel}s.
*/
public final class StandaloneModel {
private final List<BakedQuad> quads;
private final boolean useBlockLight;
private final TextureAtlasSprite particleIcon;
private final RenderType renderType;
private final Supplier<Vector3f[]> extents;
/**
* Construct a new {@link StandaloneModel}.
*
* @param quads The list of quads which form this model.
* @param usesBlockLight Whether this uses block lighting. See {@link ItemStackRenderState.LayerRenderState#setUsesBlockLight(boolean)}.
* @param particleIcon The sprite for the model's particles. See {@link ItemStackRenderState.LayerRenderState#setParticleIcon(TextureAtlasSprite)}.
* @param renderType The render type for this model.
*/
public StandaloneModel(List<BakedQuad> quads, boolean usesBlockLight, TextureAtlasSprite particleIcon, RenderType renderType) {
this.quads = quads;
this.useBlockLight = usesBlockLight;
this.particleIcon = particleIcon;
this.renderType = renderType;
this.extents = Suppliers.memoize(() -> BlockModelWrapper.computeExtents(quads));
}
/**
* Load a model from a {@link ModelBaker} and bake it.
*
* @param model The model id to load.
* @param baker The model baker.
* @return The baked {@link StandaloneModel}.
*/
public static StandaloneModel of(ResourceLocation model, ModelBaker baker) {
return of(baker.getModel(model), baker);
}
/**
* Bake a {@link ResolvedModel} into a {@link StandaloneModel}.
*
* @param model The resolved model.
* @param baker The model baker.
* @return The baked {@link StandaloneModel}.
*/
public static StandaloneModel of(ResolvedModel model, ModelBaker baker) {
return baker.compute(new CacheKey(model));
}
private record CacheKey(ResolvedModel model) implements ModelBaker.SharedOperationKey<StandaloneModel> {
@Override
public StandaloneModel compute(ModelBaker baker) {
return ofUncached(model(), baker);
}
@Override
public boolean equals(Object other) {
return other instanceof CacheKey(var otherModel) && model() == otherModel;
}
@Override
public int hashCode() {
return System.identityHashCode(model());
}
}
private static StandaloneModel ofUncached(ResolvedModel model, ModelBaker baker) {
var slots = model.getTopTextureSlots();
return new StandaloneModel(
model.bakeTopGeometry(slots, baker, BlockModelRotation.X0_Y0).getAll(),
model.getTopGuiLight().lightLikeBlock(),
model.resolveParticleSprite(slots, baker),
Sheets.translucentItemSheet()
);
}
/**
* Set up an {@link ItemStackRenderState.LayerRenderState} to render this model.
*
* @param layer The layer to set up.
* @see ItemModel#update(ItemStackRenderState, ItemStack, ItemModelResolver, ItemDisplayContext, ClientLevel, LivingEntity, int)
*/
public void setupItemLayer(ItemStackRenderState.LayerRenderState layer) {
layer.setExtents(extents);
layer.setRenderType(renderType);
layer.setUsesBlockLight(useBlockLight);
layer.setParticleIcon(particleIcon);
layer.prepareQuadList().addAll(quads);
}
/**
* Render the model directly.
*
* @param transform The current pose stack transformations.
* @param buffers The buffer source to use for rendering.
* @param light The current light texture coordinate.
* @param overlay The current overlay texture coordinate.
*/
public void render(PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
render(transform, buffers, light, overlay, null);
}
/**
* Render the model directly.
*
* @param transform The current pose stack transformations.
* @param buffers The buffer source to use for rendering.
* @param light The current light texture coordinate.
* @param overlay The current overlay texture coordinate.
* @param tints The tints for this model.
*/
public void render(PoseStack transform, MultiBufferSource buffers, int light, int overlay, int @Nullable [] tints) {
var pose = transform.last();
var buffer = buffers.getBuffer(renderType);
for (var quad : quads) {
float r, g, b, a;
var idx = quad.tintIndex();
if (tints != null && idx >= 0 && idx < tints.length) {
var tint = tints[idx];
r = ARGB.red(tint) / 255.0f;
g = ARGB.green(tint) / 255.0f;
b = ARGB.blue(tint) / 255.0f;
a = ARGB.alpha(tint) / 255.0f;
} else {
r = g = b = a = 1.0f;
}
buffer.putBulkData(pose, quad, r, g, b, a, light, overlay);
}
}
}

View File

@@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client;
import com.mojang.math.Transformation;
import dan200.computercraft.impl.client.ClientPlatformHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/**
* A model to render, combined with a transformation matrix to apply.
*/
public sealed interface TransformedModel permits TransformedModel.Baked, TransformedModel.Item {
record Baked(BakedModel model) implements TransformedModel {
}
record Item(ItemStack stack, Transformation transformation) implements TransformedModel {
}
static TransformedModel of(BakedModel model) {
return new TransformedModel.Baked(model);
}
/**
* Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
*
* @param location The location of the model to load.
* @return The new {@link TransformedModel} instance.
*/
static TransformedModel of(ResourceLocation location) {
var modelManager = Minecraft.getInstance().getModelManager();
return of(ClientPlatformHelper.get().getModel(modelManager, location));
}
static TransformedModel of(ItemStack item, Transformation transform) {
return new TransformedModel.Item(item, transform);
}
}

View File

@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.BlockModelWrapper;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.resources.ResourceLocation;
/**
* A {@link TurtleUpgradeModel} that renders a basic model.
* <p>
* This is the {@link TurtleUpgradeModel} equivalent of {@link BlockModelWrapper}.
*/
public final class BasicUpgradeModel implements TurtleUpgradeModel {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sided");
public static final MapCodec<? extends TurtleUpgradeModel.Unbaked> CODEC = RecordCodecBuilder.<Unbaked>mapCodec(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("left").forGetter(Unbaked::left),
ResourceLocation.CODEC.fieldOf("right").forGetter(Unbaked::right)
).apply(instance, Unbaked::new));
private final StandaloneModel left;
private final StandaloneModel right;
private BasicUpgradeModel(StandaloneModel left, StandaloneModel right) {
this.left = left;
this.right = right;
}
/**
* Create an unbaked {@link BasicUpgradeModel}.
*
* @param left The model when equipped on the left.
* @param right The model when equipped on the right.
* @return The unbaked turtle upgrade model.
*/
public static TurtleUpgradeModel.Unbaked unbaked(ResourceLocation left, ResourceLocation right) {
return new Unbaked(left, right);
}
private StandaloneModel getModel(TurtleSide side) {
return switch (side) {
case LEFT -> left;
case RIGHT -> right;
};
}
@Override
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
renderer.appendModelIdentityElement(this);
renderer.appendModelIdentityElement(side);
renderer.appendModelIdentityElement(transform);
var layer = renderer.newLayer();
layer.setTransform(transform);
getModel(side).setupItemLayer(layer);
}
@Override
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
getModel(side).render(transform, buffers, light, overlay);
}
private record Unbaked(ResourceLocation left, ResourceLocation right) implements TurtleUpgradeModel.Unbaked {
@Override
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
return CODEC;
}
@Override
public TurtleUpgradeModel bake(ModelBaker baker) {
return new BasicUpgradeModel(StandaloneModel.of(left(), baker), StandaloneModel.of(right(), baker));
}
@Override
public void resolveDependencies(Resolver resolver) {
resolver.markDependency(left());
resolver.markDependency(right());
}
}
}

View File

@@ -0,0 +1,143 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.mojang.math.Transformation;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
import net.minecraft.client.renderer.special.SpecialModelRenderer;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.jspecify.annotations.Nullable;
import java.util.Set;
/**
* A sic {@link TurtleUpgradeModel} that renders the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)
* upgrade item}.
* <p>
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
*/
public final class ItemUpgradeModel implements TurtleUpgradeModel {
private static final TurtleUpgradeModel.Unbaked UNBAKED = new Unbaked();
private static final TurtleUpgradeModel INSTANCE = new ItemUpgradeModel();
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item");
public static final MapCodec<TurtleUpgradeModel.Unbaked> CODEC = MapCodec.unit(UNBAKED);
private static final TransformedRenderer LEFT = computeRenderer(TurtleSide.LEFT);
private static final TransformedRenderer RIGHT = computeRenderer(TurtleSide.RIGHT);
private ItemUpgradeModel() {
}
/**
* Get the unbaked {@link ItemUpgradeModel}.
*
* @return The unbaked item upgrade model.
*/
public static TurtleUpgradeModel.Unbaked unbaked() {
return UNBAKED;
}
@Override
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
renderer.appendModelIdentityElement(this);
var childState = new TrackingItemStackRenderState();
resolver.updateForTopItem(childState, upgrade.getUpgradeItem(), ItemDisplayContext.NONE, null, null, seed);
if (!childState.isEmpty()) {
renderer.appendModelIdentityElement(childState.getModelIdentity());
renderer.appendModelIdentityElement(transform);
var layer = renderer.newLayer();
layer.setTransform(transform);
layer.setupSpecialModel(getRenderer(side), childState);
}
}
@Override
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
transform.mulPose(getRenderer(side).transform().getMatrix());
transform.mulPose(Axis.YP.rotation(Mth.PI));
Minecraft.getInstance().getItemRenderer().renderStatic(
upgrade.getUpgradeItem(), ItemDisplayContext.FIXED, light, overlay, transform, buffers, turtle.getLevel(), 0
);
}
private static final class Unbaked implements TurtleUpgradeModel.Unbaked {
@Override
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
return CODEC;
}
@Override
public TurtleUpgradeModel bake(ModelBaker baker) {
return INSTANCE;
}
@Override
public void resolveDependencies(Resolver resolver) {
}
}
private static TransformedRenderer computeRenderer(TurtleSide side) {
var pose = new Matrix4f();
pose.translate(0.5f, 0.5f, 0.5f);
pose.rotate(Axis.YN.rotationDegrees(90f));
pose.rotate(Axis.ZP.rotationDegrees(90f));
pose.translate(0.0f, 0.0f, side == TurtleSide.RIGHT ? -0.4065f : 0.4065f);
return new TransformedRenderer(new Transformation(pose));
}
private static TransformedRenderer getRenderer(TurtleSide side) {
return switch (side) {
case LEFT -> LEFT;
case RIGHT -> RIGHT;
};
}
private record TransformedRenderer(Transformation transform) implements SpecialModelRenderer<ItemStackRenderState> {
@Override
public void render(
@Nullable ItemStackRenderState state, ItemDisplayContext itemDisplayContext, PoseStack poseStack,
MultiBufferSource multiBufferSource, int overlay, int light, boolean bl
) {
if (state == null) return;
poseStack.pushPose();
poseStack.mulPose(transform.getMatrix());
state.render(poseStack, multiBufferSource, overlay, light);
poseStack.popPose();
}
@Override
public void getExtents(Set<Vector3f> set) {
}
@Override
public @Nullable ItemStackRenderState extractArgument(ItemStack itemStack) {
return null;
}
}
}

View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.serialization.MapCodec;
import net.minecraft.resources.ResourceLocation;
/**
* A functional interface to register a {@link TurtleUpgradeModel}.
* <p>
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
* multiple loaders.
*/
@FunctionalInterface
public interface RegisterTurtleUpgradeModel {
/**
* Register a {@link TurtleUpgradeModel}.
*
* @param id The id used for this type of upgrade model.
* @param model The codec used to read/decode an upgrade model.
*/
void register(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> model);
}

View File

@@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
/**
* A functional interface to register a {@link TurtleUpgradeModeller} for a class of turtle upgrades.
* <p>
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
* multiple loaders.
*/
@FunctionalInterface
public interface RegisterTurtleUpgradeModeller {
/**
* Register a {@link TurtleUpgradeModeller}.
*
* @param type The turtle upgrade type.
* @param modeller The upgrade modeller.
* @param <T> The type of the turtle upgrade.
*/
<T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModeller<T> modeller);
}

View File

@@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.Util;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.item.SelectItemModel;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
/**
* A {@link TurtleUpgradeModel} which selects between different models based on the value of a component in
* {@linkplain UpgradeData#data() the upgrade's data}.
* <p>
* This is the {@link TurtleUpgradeModel} equivalent of {@link SelectItemModel}.
*
* @param <T> The type of value to switch on.
*/
public final class SelectUpgradeModel<T> implements TurtleUpgradeModel {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "select");
public static final MapCodec<? extends TurtleUpgradeModel.Unbaked> CODEC = RecordCodecBuilder.<Unbaked<?>>mapCodec(instance -> instance.group(
Cases.CODEC.forGetter(Unbaked::cases),
TurtleUpgradeModel.CODEC.optionalFieldOf("fallback").forGetter(Unbaked::fallback)
).apply(instance, Unbaked::new));
private final DataComponentType<T> component;
private final Map<T, TurtleUpgradeModel> cases;
private final TurtleUpgradeModel fallback;
private SelectUpgradeModel(DataComponentType<T> component, Map<T, TurtleUpgradeModel> cases, TurtleUpgradeModel fallback) {
this.component = component;
this.cases = cases;
this.fallback = fallback;
}
private TurtleUpgradeModel getModel(UpgradeData<ITurtleUpgrade> upgrade) {
var value = upgrade.get(component);
if (value == null) return fallback;
var model = cases.get(value);
return model != null ? model : fallback;
}
@Override
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
getModel(upgrade).renderForItem(upgrade, side, renderer, resolver, transform, seed);
}
@Override
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
getModel(upgrade).renderForLevel(upgrade, side, turtle, transform, buffers, light, overlay);
}
private record Unbaked<T>(
Cases<T> cases,
Optional<TurtleUpgradeModel.Unbaked> fallback
) implements TurtleUpgradeModel.Unbaked {
private static final TurtleUpgradeModel.Unbaked MISSING = BasicUpgradeModel.unbaked(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION);
@Override
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
return CODEC;
}
@Override
public TurtleUpgradeModel bake(ModelBaker baker) {
Map<T, TurtleUpgradeModel> cases = new Object2ObjectOpenHashMap<>();
for (var condition : cases().cases()) {
var model = condition.getSecond().bake(baker);
for (var when : condition.getFirst()) cases.put(when, model);
}
return new SelectUpgradeModel<>(cases().component(), cases, fallback().orElse(MISSING).bake(baker));
}
@Override
public void resolveDependencies(Resolver resolver) {
cases().cases().forEach(x -> x.getSecond().resolveDependencies(resolver));
fallback().orElse(MISSING).resolveDependencies(resolver);
}
}
private record Cases<T>(DataComponentType<T> component, List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases) {
private static final MapCodec<Cases<?>> CODEC = DataComponentType.CODEC.dispatchMap("property", Cases::component, Util.memoize(Cases::codec));
private static <T> MapCodec<Cases<T>> codec(DataComponentType<T> component) {
return RecordCodecBuilder.mapCodec(instance -> instance.group(
MapCodec.unit(component).forGetter(Cases::component),
caseCodec(component.codecOrThrow()).listOf().fieldOf("cases").validate(Cases::validate).forGetter(Cases::cases)
).apply(instance, Cases<T>::new));
}
private static <T> Codec<Pair<List<T>, TurtleUpgradeModel.Unbaked>> caseCodec(Codec<T> codec) {
return RecordCodecBuilder.create(instance -> instance.group(
codec.listOf().fieldOf("when").forGetter(Pair::getFirst),
TurtleUpgradeModel.CODEC.fieldOf("model").forGetter(Pair::getSecond)
).apply(instance, Pair::new));
}
private static <T> DataResult<List<Pair<List<T>, TurtleUpgradeModel.Unbaked>>> validate(List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases) {
Multiset<T> multiset = HashMultiset.create();
for (var condition : cases) multiset.addAll(condition.getFirst());
if (multiset.isEmpty()) return DataResult.error(() -> "Empty cases");
if (multiset.size() != multiset.entrySet().size()) {
return DataResult.error(() -> "Duplicate case conditions: " + multiset.entrySet().stream()
.filter(x -> x.getCount() > 1)
.map(x -> Objects.toString(x.getElement()))
.collect(Collectors.joining(", ")));
}
return DataResult.success(cases);
}
}
/**
* Create a {@link SelectUpgradeModel} that selects a model based on a component.
*
* @param component The component to select.
* @param <T> The type the component stores.
* @return A {@link Builder}.
*/
public static <T> Builder<T> onComponent(DataComponentType<T> component) {
return new Builder<>(component);
}
/**
* A builder for constructing {@link SelectUpgradeModel}s.
*
* @param <T> The type of value to switch on.
*/
public static final class Builder<T> {
private final DataComponentType<T> component;
private final List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases = new ArrayList<>();
private TurtleUpgradeModel.@Nullable Unbaked fallback;
private Builder(DataComponentType<T> component) {
this.component = component;
}
/**
* Add a case to our model.
*
* @param value The value for this case.
* @param model The model to use.
* @return {@code this}, for chaining.
*/
public Builder<T> when(T value, TurtleUpgradeModel.Unbaked model) {
return when(List.of(value), model);
}
/**
* Add a case to our model.
*
* @param values The value(s) for this case.
* @param model The model to use.
* @return {@code this}, for chaining.
*/
public Builder<T> when(List<T> values, TurtleUpgradeModel.Unbaked model) {
cases.add(Pair.of(values, model));
return this;
}
/**
* Add a fallback value, when no previous value matches or the component is not present.
*
* @param model The fallback model.
* @return {@code this}, for chaining.
*/
public Builder<T> fallback(TurtleUpgradeModel.Unbaked model) {
this.fallback = model;
return this;
}
/**
* Convert this builder into an unbaked model.
*
* @return The unbaked {@link SelectUpgradeModel}.
*/
public TurtleUpgradeModel.Unbaked create() {
return new Unbaked<>(new Cases<>(component, cases), Optional.ofNullable(fallback));
}
}
}

View File

@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.client.ComputerCraftAPIClientService;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
/**
* The model for a {@link ITurtleUpgrade}.
* <p>
* Turtle upgrade models are very similar to vanilla's {@link ItemModel}. Each upgrade's model is defined in JSON, and
* loaded from resource packs with other assets.
* <p>
* In most cases, upgrades can use one of the existing implementations of {@link TurtleUpgradeModel} (e.g.
* {@link BasicUpgradeModel} or {@link ItemUpgradeModel}), and do not need to subclass it. However, in the cases where
* a custom model is required, one should use
* {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModel} to register a
* model on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent} to register one
* on Forge.
* <p>
* See {@link ITurtleUpgrade} for a full example of registering turtle upgrades and their models.
*
* @see RegisterTurtleUpgradeModel For multi-loader registration support.
* @see ItemUpgradeModel A {@code TurtleUpgradeModel} which uses the upgrade's item.
* @see BasicUpgradeModel A {@code TurtleUpgradeModel} which renders a simple model.
*/
public interface TurtleUpgradeModel {
/**
* The directory from which turtle upgrade models are loaded. This may be used by data generators.
*/
String SOURCE = ComputerCraftAPI.MOD_ID + "/turtle_upgrade";
/**
* The codec used to read/write {@linkplain TurtleUpgradeModel.Unbaked unbaked upgrade models}.
*/
Codec<TurtleUpgradeModel.Unbaked> CODEC = Codec.lazyInitialized(() -> ComputerCraftAPIClientService.get().getTurtleUpgradeModelCodec());
/**
* Render this upgrade to an {@link ItemStackRenderState}. This is used for rendering the item form of the upgrade.
* <p>
* Like with {@link ItemModel}, implementations must be careful to call {@link ItemStackRenderState#appendModelIdentityElement}
* where appropriate.
*
* @param upgrade The upgrade being rendered.
* @param side Which side of the turtle (left or right) the upgrade is equipped on.
* @param renderer The render state to draw to.
* @param resolver The model resolver.
* @param transform The root model's transformation.
* @param seed The current model seed.
* @see ItemModel#update(ItemStackRenderState, ItemStack, ItemModelResolver, ItemDisplayContext, ClientLevel, LivingEntity, int)
*/
void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed);
/**
* Render this upgrade to a {@link MultiBufferSource}. This is used for rendering the block-entity form of the
* upgrade.
*
* @param upgrade The upgrade being rendered.
* @param side Which side of the turtle (left or right) the upgrade is equipped on.
* @param turtle Access to the turtle that the upgrade resides on.
* @param transform The current pose stack.
* @param buffers The buffers to render to.
* @param light The lightmap coordinate.
* @param overlay The overlay coordinate.
*/
void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay);
/**
* An unbaked turtle model. Much like other unbaked models (e.g. {@link ItemModel.Unbaked}), this should resolve
* any dependencies and returned the fully-resolved model.
*/
interface Unbaked extends ResolvableModel {
/**
* The {@link MapCodec} used to read/write this unbaked model.
*
* @return The codec used to read/write this model.
* @see ItemModel.Unbaked#type()
*/
MapCodec<? extends Unbaked> type();
/**
* Bake this turtle model.
*
* @param baker The current model baker
* @return The baked upgrade model.
* @see ItemModel.Unbaked#bake(ItemModel.BakingContext)
*/
TurtleUpgradeModel bake(ModelBaker baker);
}
}

View File

@@ -1,99 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
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 java.util.stream.Stream;
/**
* Provides models for a {@link ITurtleUpgrade}.
* <p>
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
* on Forge.
*
* <h2>Example</h2>
* <h3>Fabric</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h3>Forge</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* @param <T> The type of turtle upgrade this modeller applies to.
* @see RegisterTurtleUpgradeModeller For multi-loader registration support.
*/
public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
/**
* Obtain the model to be used when rendering a turtle peripheral.
* <p>
* When the current turtle is {@literal null}, this function should be constant for a given upgrade, side and data.
*
* @param upgrade The upgrade that you're getting the model for.
* @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models.
* @param side Which side of the turtle (left or right) the upgrade resides on.
* @param data Upgrade data instance for current turtle side.
* @return The model that you wish to be used to render your upgrade.
*/
TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data);
/**
* Get the models that this turtle modeller depends on.
* <p>
* Models included in this stream will be loaded and baked alongside item and block models, and so may be referenced
* by {@link TransformedModel#of(ResourceLocation)}. You do not need to override this method if you will load models
* by other means.
*
* @return A list of models that this modeller depends on.
* @see UnbakedModel#resolveDependencies(UnbakedModel.Resolver)
*/
default Stream<ResourceLocation> getDependencies() {
return Stream.of();
}
/**
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)}
* upgrade item}.
* <p>
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
*
* @param <T> The type of the turtle upgrade.
* @return The constructed modeller.
*/
@SuppressWarnings("unchecked")
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> flatItem() {
return (TurtleUpgradeModeller<T>) TurtleUpgradeModellers.UPGRADE_ITEM;
}
/**
* Construct a {@link TurtleUpgradeModeller} which has a single model for the left and right side.
*
* @param left The model to use on the left.
* @param right The model to use on the right.
* @param <T> The type of the turtle upgrade.
* @return The constructed modeller.
*/
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ResourceLocation left, ResourceLocation right) {
return new TurtleUpgradeModeller<>() {
@Override
public TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
return TransformedModel.of(side == TurtleSide.LEFT ? left : right);
}
@Override
public Stream<ResourceLocation> getDependencies() {
return Stream.of(left, right);
}
};
}
}

View File

@@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.math.Axis;
import com.mojang.math.Transformation;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import net.minecraft.core.component.DataComponentPatch;
import org.joml.Matrix4f;
import org.jspecify.annotations.Nullable;
final class TurtleUpgradeModellers {
private static final Transformation leftTransform = getMatrixFor(TurtleSide.LEFT);
private static final Transformation rightTransform = getMatrixFor(TurtleSide.RIGHT);
private static Transformation getMatrixFor(TurtleSide side) {
var pose = new Matrix4f();
pose.translate(0.5f, 0.5f, 0.5f);
pose.rotate(Axis.YN.rotationDegrees(90f));
pose.rotate(Axis.ZP.rotationDegrees(90f));
pose.translate(0.0f, 0.0f, side == TurtleSide.RIGHT ? -0.4065f : 0.4065f);
return new Transformation(pose);
}
static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
private static final class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
@Override
public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
return TransformedModel.of(upgrade.getUpgradeItem(data), side == TurtleSide.LEFT ? leftTransform : rightTransform);
}
}
}

View File

@@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.client;
import dan200.computercraft.impl.Services;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
@ApiStatus.Internal
public interface ClientPlatformHelper {
/**
* Get a model from a resource.
*
* @param manager The model manager.
* @param resourceLocation The model resourceLocation.
* @return The baked model.
*/
BakedModel getModel(ModelManager manager, ResourceLocation resourceLocation);
static ClientPlatformHelper get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
}
final class Instance {
static final @Nullable ClientPlatformHelper INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(ClientPlatformHelper.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
}

View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.client;
import com.mojang.serialization.Codec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.impl.Services;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
/**
* Bridge between implementation
* <p>
* Do <strong>NOT</strong> directly reference this class. It exists for internal use by the API.
*/
@ApiStatus.Internal
public interface ComputerCraftAPIClientService {
static ComputerCraftAPIClientService get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ComputerCraftAPIClientService.class, Instance.ERROR) : instance;
}
Codec<TurtleUpgradeModel.Unbaked> getTurtleUpgradeModelCodec();
final class Instance {
static final @Nullable ComputerCraftAPIClientService INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(ComputerCraftAPIClientService.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
}

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,6 +6,7 @@ package dan200.computercraft.api.component;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.pocket.IPocketAccess;
import dan200.computercraft.api.pocket.PocketComputer;
import dan200.computercraft.api.turtle.ITurtleAccess;
/**
@@ -20,7 +21,7 @@ public class ComputerComponents {
/**
* The {@link IPocketAccess} associated with a pocket computer.
*/
public static final ComputerComponent<IPocketAccess> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
public static final ComputerComponent<PocketComputer> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
/**
* This component is only present on "command computers", and other computers with admin capabilities.

View File

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

@@ -7,60 +7,15 @@ package dan200.computercraft.api.pocket;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
/**
* Wrapper class for pocket computers.
* Access to a pocket computer for {@linkplain IPocketUpgrade pocket upgrades}.
*/
@ApiStatus.NonExtendable
public interface IPocketAccess {
/**
* Get the level in which the pocket computer exists.
*
* @return The pocket computer's level.
*/
ServerLevel getLevel();
/**
* Get the position of the pocket computer.
*
* @return The pocket computer's position.
*/
Vec3 getPosition();
/**
* Gets the entity holding this item.
* <p>
* This must be called on the server thread.
*
* @return The holding entity, or {@code null} if none exists.
*/
@Nullable
Entity getEntity();
/**
* Get the colour of this pocket computer as a RGB number.
*
* @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and
* {@code 0xFFFFFF} or -1 if it has no colour.
* @see #setColour(int)
*/
int getColour();
/**
* Set the colour of the pocket computer to a RGB number.
*
* @param colour The colour this pocket computer should be changed to. This should be a RGB colour between
* {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour.
* @see #getColour()
*/
void setColour(int colour);
public interface IPocketAccess extends PocketComputer {
/**
* Get the colour of this pocket computer's light as a RGB number.
*
@@ -92,7 +47,8 @@ public interface IPocketAccess {
/**
* Set the upgrade for this pocket computer, also updating the item stack.
* <p>
* Note this method is not thread safe - it must be called from the server thread.
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
* active}.
*
* @param upgrade The new upgrade to set it to, may be {@code null}.
* @see #getUpgrade()
@@ -114,6 +70,9 @@ public interface IPocketAccess {
/**
* Update the upgrade-specific data.
* <p>
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
* active}.
*
* @param data The new upgrade data.
* @see #getUpgradeData()

View File

@@ -12,7 +12,7 @@ import dan200.computercraft.impl.ComputerCraftAPIService;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.server.level.ServerLevel;
import org.jspecify.annotations.Nullable;
/**
@@ -71,7 +71,7 @@ public interface IPocketUpgrade extends UpgradeBase {
/**
* Called when the pocket computer is right clicked.
*
* @param world The world the computer is in.
* @param level The world the computer is in.
* @param access The access object for the pocket item stack.
* @param peripheral The peripheral for this upgrade.
* @return {@code true} to stop the GUI from opening, otherwise false. You should always provide some code path
@@ -79,7 +79,7 @@ public interface IPocketUpgrade extends UpgradeBase {
* access the GUI.
* @see #createPeripheral(IPocketAccess)
*/
default boolean onRightClick(Level world, IPocketAccess access, @Nullable IPeripheral peripheral) {
default boolean onRightClick(ServerLevel level, IPocketAccess access, @Nullable IPeripheral peripheral) {
return false;
}
}

View File

@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.pocket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
/**
* A pocket computer.
*
* @see IPocketAccess
* @see dan200.computercraft.api.component.ComputerComponents#POCKET
*/
@ApiStatus.NonExtendable
public interface PocketComputer {
/**
* Get the level in which the pocket computer exists.
*
* @return The pocket computer's level.
*/
ServerLevel getLevel();
/**
* Get the position of the pocket computer.
*
* @return The pocket computer's position.
*/
Vec3 getPosition();
/**
* Gets the entity holding this item.
* <p>
* This must be called on the server thread.
*
* @return The holding entity, or {@code null} if none exists.
*/
@Nullable
Entity getEntity();
/**
* Check whether this pocket computer is currently being held by a player, lectern, or other valid entity.
* <p>
* As pocket computers are backed by item stacks, you must check for validity before updating the computer.
* <p>
* This must be called on the server thread.
*
* @return Whether this computer is active.
*/
boolean isActive();
/**
* Get the colour of this pocket computer as an RGB number.
*
* <p>
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
* active}.
*
* @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and
* {@code 0xFFFFFF} or -1 if it has no colour.
* @see #setColour(int)
*/
int getColour();
/**
* Set the colour of the pocket computer to an RGB number.
* <p>
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
* active}.
*
* @param colour The colour this pocket computer should be changed to. This should be a RGB colour between
* {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour.
* @see #getColour()
*/
void setColour(int colour);
}

View File

@@ -51,16 +51,6 @@ import java.util.function.Function;
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.ForgeExampleMod region=turtle_upgrades}
*
* <h3>Rendering the upgrade</h3>
* Next, we need to register a model for our upgrade. This is done by registering a
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for your upgrade type.
*
* <h4>Fabric</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h3 id="datagen">Registering the upgrade itself</h3>
* Upgrades themselves are loaded from datapacks when a level is loaded. In order to register our new upgrade, we must
* create a new JSON file at {@code data/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
@@ -71,8 +61,15 @@ import java.util.function.Function;
* by the type itself. As our upgrade was defined with {@link UpgradeType#simpleWithCustomItem(Function)}, the
* {@code "item"} field will construct our upgrade with {@link Items#COMPASS}.
* <p>
* Rather than manually creating the file, it is recommended to use data-generators to generate this file. First, we
* register our new upgrades into a {@linkplain PatchedRegistries patched registry}.
* Similarly, {@linkplain dan200.computercraft.api.client.turtle.TurtleUpgradeModel the upgrade's model} is loaded from
* a resource pack, and so we must also create a new JSON file at
* {@code assets/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
*
* {@snippet file=assets/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json}
*
* Rather than manually creating these file, it is recommended to use data-generators to generate this file. First, we
* register our new upgrades into a {@linkplain PatchedRegistries patched registry}. Models must similarly be
* registered.
*
* {@snippet class=com.example.examplemod.data.TurtleUpgradeProvider region=body}
*

View File

@@ -7,8 +7,11 @@ package dan200.computercraft.api.upgrades;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentGetter;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* An upgrade (i.e. a {@link ITurtleUpgrade}) and its current upgrade data.
@@ -17,7 +20,9 @@ import net.minecraft.world.item.ItemStack;
* @param data The upgrade's data.
* @param <T> The type of upgrade, either {@link ITurtleUpgrade} or {@link IPocketUpgrade}.
*/
public record UpgradeData<T extends UpgradeBase>(Holder.Reference<T> holder, DataComponentPatch data) {
public record UpgradeData<T extends UpgradeBase>(
Holder.Reference<T> holder, DataComponentPatch data
) implements DataComponentGetter {
/**
* A utility method to construct a new {@link UpgradeData} instance.
*
@@ -66,4 +71,17 @@ public record UpgradeData<T extends UpgradeBase>(Holder.Reference<T> holder, Dat
public ItemStack getUpgradeItem() {
return upgrade().getUpgradeItem(data).copy();
}
/**
* Get a component from the upgrade's {@link #data()} .
*
* @param component The component get.
* @param <U> The type of the component's value.
* @return The component, or {@code null} if not present.
*/
@Override
public <U> @Nullable U get(DataComponentType<? extends U> component) {
var result = data().get(component);
return result == null ? null : result.orElse(null);
}
}

View File

@@ -14,7 +14,6 @@ plugins {
sourceSets.client {
java {
exclude("dan200/computercraft/client/integration/emi")
exclude("dan200/computercraft/client/integration/jei")
}
}

View File

@@ -39,7 +39,6 @@ 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 java.util.function.Consumer;
@@ -137,20 +136,20 @@ public final class ClientHooks {
if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.holder().key().location()));
}
public static @Nullable BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
public static BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
// Only apply to cables which have both a cable and modem
if (state.getBlock() != ModRegistry.Blocks.CABLE.get()
|| !state.getValue(CableBlock.CABLE)
|| state.getValue(CableBlock.MODEM) == CableModemVariant.None
) {
return null;
return state;
}
var hit = Minecraft.getInstance().hitResult;
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return null;
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return state;
var hitPos = ((BlockHitResult) hit).getBlockPos();
if (!hitPos.equals(pos)) return null;
if (!hitPos.equals(pos)) return state;
return WorldUtil.isVecInside(CableShapes.getModemShape(state), hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()))
? state.getBlock().defaultBlockState().setValue(CableBlock.MODEM, state.getValue(CableBlock.MODEM))

View File

@@ -4,51 +4,53 @@
package dan200.computercraft.client;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.*;
import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.item.colour.PocketComputerLight;
import dan200.computercraft.client.item.model.TurtleOverlayModel;
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
import dan200.computercraft.client.item.properties.PocketComputerStateProperty;
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.client.render.CustomLecternRenderer;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleModemModeller;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemTintSource;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
import net.minecraft.client.gui.screens.MenuScreens;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
import net.minecraft.network.chat.Component;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import java.io.File;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
@@ -62,6 +64,19 @@ public final class ClientRegistry {
private ClientRegistry() {
}
private static final Map<ResourceLocation, ModelKey<StandaloneModel>> models = new ConcurrentHashMap<>();
public static ModelKey<StandaloneModel> getModel(ResourceLocation model) {
return models.computeIfAbsent(model, m -> ClientPlatformHelper.get().createModelKey(m::toString));
}
public static StandaloneModel getModel(ModelManager manager, ResourceLocation modelId) {
var model = getModel(modelId).get(manager);
if (model != null) return model;
return Objects.requireNonNull(getModel(MissingBlockModel.LOCATION).get(manager));
}
/**
* Register any client-side objects which don't have to be done on the main thread.
*/
@@ -87,21 +102,10 @@ public final class ClientRegistry {
<M extends AbstractContainerMenu, U extends Screen & MenuAccess<M>> void register(MenuType<? extends M> type, MenuScreens.ScreenConstructor<M, U> factory);
}
public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
register.register(ModRegistry.TurtleUpgradeTypes.SPEAKER.get(), TurtleUpgradeModeller.sided(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
register.register(ModRegistry.TurtleUpgradeTypes.WORKBENCH.get(), TurtleUpgradeModeller.sided(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
register.register(ModRegistry.TurtleUpgradeTypes.WIRELESS_MODEM.get(), new TurtleModemModeller());
register.register(ModRegistry.TurtleUpgradeTypes.TOOL.get(), TurtleUpgradeModeller.flatItem());
}
public static void registerReloadListeners(BiConsumer<ResourceLocation, PreparableReloadListener> register, Minecraft minecraft) {
register.accept(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sprites"), GuiSprites.initialise(minecraft.getTextureManager()));
public static void registerTurtleModels(RegisterTurtleUpgradeModel register) {
register.register(BasicUpgradeModel.ID, BasicUpgradeModel.CODEC);
register.register(ItemUpgradeModel.ID, ItemUpgradeModel.CODEC);
register.register(SelectUpgradeModel.ID, SelectUpgradeModel.CODEC);
}
private static final ResourceLocation[] EXTRA_MODELS = {
@@ -109,17 +113,72 @@ public final class ClientRegistry {
TurtleBlockEntityRenderer.NORMAL_TURTLE_MODEL,
TurtleBlockEntityRenderer.ADVANCED_TURTLE_MODEL,
TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
MissingBlockModel.LOCATION,
};
public static void registerExtraModels(Consumer<ResourceLocation> register, Collection<ResourceLocation> extraModels) {
for (var model : EXTRA_MODELS) register.accept(model);
extraModels.forEach(register);
TurtleUpgradeModellers.getDependencies().forEach(register);
/**
* Additional models to load.
*
* @param turtleOverlays The unbaked turtle models.
* @param turtleUpgrades The unbaked turtle upgrades.
* @see #gatherExtraModels(ResourceManager, Executor)
* @see #registerExtraModels(RegisterExtraModels, ExtraModels)
*/
public record ExtraModels(
Map<ResourceLocation, TurtleOverlay.Unbaked> turtleOverlays,
Map<ResourceLocation, TurtleUpgradeModel.Unbaked> turtleUpgrades
) {
}
/**
* Gather the list of extra models to load.
*
* @param resources The current resource manager.
* @param executor The executor to schedule loading on.
* @return A promise which contains our extra models.
*/
public static CompletableFuture<ExtraModels> gatherExtraModels(ResourceManager resources, Executor executor) {
var turtleOverlays = TurtleOverlayManager.loader().load(resources, executor);
var turtleUpgrades = TurtleUpgradeModelManager.loader().load(resources, executor);
return turtleOverlays.thenCombine(turtleUpgrades, ExtraModels::new);
}
/**
* A callback used to register a model for a {@link ModelKey}.
*/
public interface RegisterExtraModels {
default <U extends ResolvableModel, T> void register(ModelKey<T> key, U unbaked, BiFunction<U, ModelBaker, T> bake) {
register(key, unbaked, ResolvableModel::resolveDependencies, bake);
}
/**
* Register an extra model.
* <p>
* This accepts functions to resolve dependencies and bake the model. While this would be conceptually nicer as
* an interface, it would require multiple adaptors to convert between "upgrade model", "a"bstract model" and
* "platform-specific model", so working with functions is cleaner.
*
* @param key The model key for this model.
* @param unbaked The unbaked model.
* @param resolve The function to resolve dependencies for this model.
* @param bake The function to bake this model.
* @param <U> The type of unbaked model.
* @param <T> The type of baked model.
*/
<U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake);
}
public static void registerExtraModels(RegisterExtraModels register, ExtraModels models) {
for (var model : EXTRA_MODELS) {
register.register(getModel(model), model, (id, r) -> r.markDependency(id), StandaloneModel::of);
}
TurtleOverlayManager.loader().register(register, models.turtleOverlays());
TurtleUpgradeModelManager.loader().register(register, models.turtleUpgrades());
}
public static void registerItemModels(BiConsumer<ResourceLocation, MapCodec<? extends ItemModel.Unbaked>> register) {
register.accept(TurtleOverlayModel.ID, TurtleOverlayModel.CODEC);
register.accept(TurtleUpgradeModel.ID, TurtleUpgradeModel.CODEC);
register.accept(dan200.computercraft.client.item.model.TurtleUpgradeModel.ID, dan200.computercraft.client.item.model.TurtleUpgradeModel.CODEC);
}
public static void registerItemColours(BiConsumer<ResourceLocation, MapCodec<? extends ItemTintSource>> register) {
@@ -134,44 +193,11 @@ public final class ClientRegistry {
register.accept(TurtleShowElfOverlay.ID, TurtleShowElfOverlay.CODEC);
}
/**
* Register client-side commands.
*
* @param dispatcher The dispatcher to register the commands to.
* @param sendError A function to send an error message.
* @param <T> The type of the client-side command context.
*/
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
.then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
.executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
));
public interface RegisterPictureInPictureRenderer {
<T extends PictureInPictureRenderState> void register(Class<T> state, Function<MultiBufferSource.BufferSource, PictureInPictureRenderer<T>> factory);
}
/**
* Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
*
* @param context The command context.
* @param sendError A function to send an error message.
* @param id The computer's id.
* @param <T> The type of the client-side command context.
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
*/
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
var server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) {
sendError.accept(context, Component.literal("Not on a single-player server"));
return 0;
}
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) {
sendError.accept(context, Component.literal("Computer's folder does not exist"));
return 0;
}
Util.getPlatform().openFile(file);
return 1;
public static void registerPictureInPictureRenderers(RegisterPictureInPictureRenderer register) {
register.register(PrintoutScreen.PrintoutRenderState.class, PrintoutScreen.PrintoutPictureRenderer::new);
}
}

View File

@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.shared.util.ResourceUtils;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
/**
* A manager for loading custom models. This is responsible for {@linkplain #load(ResourceManager, Executor) loading
* models from resource packs}, {@linkplain #register(ClientRegistry.RegisterExtraModels, Map) registering them as
* extra models}, and then {@linkplain #get(ModelManager, ResourceLocation) looking them up}.
*
* @param <U> The type of unbaked model.
* @param <T> The type of baked model.
*/
public class CustomModelManager<U extends ResolvableModel, T> {
private final String kind;
private final FileToIdConverter lister;
private final Codec<U> codec;
private final BiFunction<U, ModelBaker, T> bake;
private final ModelKey<T> missingModelKey;
private final U missingModel;
private final Map<ResourceLocation, ModelKey<T>> modelKeys = new ConcurrentHashMap<>();
public CustomModelManager(String kind, FileToIdConverter lister, Codec<U> codec, BiFunction<U, ModelBaker, T> bake, U missingModel) {
this.kind = kind;
this.lister = lister;
this.codec = codec;
this.bake = bake;
this.missingModelKey = ClientPlatformHelper.get().createModelKey(() -> "Missing " + kind);
this.missingModel = missingModel;
}
private ModelKey<T> getModelKey(ResourceLocation id) {
return modelKeys.computeIfAbsent(id, o -> ClientPlatformHelper.get().createModelKey(() -> kind + " " + o));
}
/**
* Load our models from resources.
*
* @param resources The current resource manager.
* @param executor The executor to schedule work on.
* @return The map of unbaked models.
*/
public CompletableFuture<Map<ResourceLocation, U>> load(ResourceManager resources, Executor executor) {
return ResourceUtils.load(resources, executor, kind, lister, JsonOps.INSTANCE, codec);
}
/**
* Register our unbaked models.
*
* @param register The callback to register models with.
* @param models The models to register.
*/
public void register(ClientRegistry.RegisterExtraModels register, Map<ResourceLocation, U> models) {
models.forEach((id, model) -> register.register(getModelKey(id), model, bake));
register.register(missingModelKey, missingModel, bake);
}
/**
* Find the model with the given id. If the model does not exist, then the missing model is returned instead.
*
* @param modelManager The model manager.
* @param id The model id.
* @return The loaded model.
*/
public T get(ModelManager modelManager, ResourceLocation id) {
var model = getModelKey(id).get(modelManager);
if (model != null) return model;
var missing = missingModelKey.get(modelManager);
if (missing == null) throw new IllegalStateException("Models have not yet been loaded.");
return missing;
}
}

View File

@@ -6,10 +6,10 @@ 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.SpriteRenderer;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
@@ -39,14 +39,16 @@ 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 computerTextures = GuiSprites.getComputerTextures(family);
SpriteRenderer.inGui(graphics, spriteRenderer -> {
var computerTextures = GuiSprites.getComputerTextures(family);
ComputerBorderRenderer.render(
spriteRenderer, computerTextures,
terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
);
ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
});
graphics.blitSprite(
RenderPipelines.GUI_TEXTURED, computerTextures.border(),
terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2
);
graphics.blitSprite(
RenderPipelines.GUI_TEXTURED, Nullability.assertNonNull(computerTextures.sidebar()),
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
);
}
}

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@@ -24,7 +24,7 @@ public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
}
@Override

View File

@@ -7,9 +7,6 @@ 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;
@@ -19,10 +16,7 @@ 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

@@ -9,7 +9,7 @@ import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.toasts.Toast;
import net.minecraft.client.gui.components.toasts.ToastManager;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FormattedCharSequence;
@@ -91,7 +91,7 @@ public class ItemToast implements Toast {
@Override
public void render(GuiGraphics graphics, Font font, long time) {
graphics.blitSprite(RenderType::guiTextured, TEXTURE, 0, 0, width(), height());
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, width(), height());
var textX = MARGIN;
if (!stack.isEmpty()) {

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

@@ -72,8 +72,8 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
var direction = scrollHandler.onMouseScroll(scrollX, scrollY);
var inventory = Objects.requireNonNull(minecraft().player).getInventory();
inventory.setSelectedHotbarSlot(ScrollWheelHandler.getNextScrollWheelSelection(
direction.y == 0 ? -direction.x : direction.y, inventory.selected, Inventory.getSelectionSize()
inventory.setSelectedSlot(ScrollWheelHandler.getNextScrollWheelSelection(
direction.y == 0 ? -direction.x : direction.y, inventory.getSelectedSlot(), Inventory.getSelectionSize()
));
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
@@ -108,7 +108,7 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8));
var y = 10;
for (var line : lines) {
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFF, true);
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFFFF, true);
y += 9;
}
}

View File

@@ -10,7 +10,7 @@ import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
@@ -88,13 +88,13 @@ public final class OptionScreen extends Screen {
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
// Render the actual texture.
graphics.blit(RenderType::guiTextured, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
graphics.blit(RenderType::guiTextured, BACKGROUND,
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND,
x, y + PADDING, 0, PADDING, innerWidth, innerHeight - PADDING * 2,
innerWidth, PADDING,
256, 256
);
graphics.blit(RenderType::guiTextured, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
assertNonNull(messageRenderer).renderLeftAlignedNoShadow(graphics, x + PADDING, y + PADDING, FONT_HEIGHT, 0x404040);
super.render(graphics, mouseX, mouseY, partialTicks);

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
import dan200.computercraft.shared.peripheral.printer.PrinterMenu;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@@ -24,10 +24,10 @@ public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
if (getMenu().isPrinting()) {
graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
}
}

View File

@@ -4,18 +4,27 @@
package dan200.computercraft.client.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.client.render.PrintoutRenderer;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.PrintoutMenu;
import dan200.computercraft.shared.media.items.PrintoutData;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import net.minecraft.client.gui.render.state.GuiElementRenderState;
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.item.ItemStack;
import org.joml.Matrix3x2f;
import org.jspecify.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
@@ -25,7 +34,7 @@ import static dan200.computercraft.client.render.PrintoutRenderer.*;
/**
* The GUI for printed pages and books.
*
* @see dan200.computercraft.client.render.PrintoutRenderer
* @see PrintoutRenderer
*/
public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu> implements ContainerListener {
private PrintoutInfo printout = PrintoutInfo.DEFAULT;
@@ -113,15 +122,11 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
// Push the printout slightly forward, to avoid clipping into the background.
graphics.pose().pushPose();
graphics.pose().translate(0, 0, 1);
graphics.drawSpecial(bufferSource -> {
drawBorder(graphics.pose(), bufferSource, leftPos, topPos, 0, page, printout.pages(), printout.book(), LightTexture.FULL_BRIGHT);
drawText(graphics.pose(), bufferSource, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, LightTexture.FULL_BRIGHT, printout.text(), printout.colour());
});
graphics.pose().popPose();
graphics.guiRenderState.submitPicturesInPictureState(new PrintoutRenderState(
leftPos - COVER_SIZE - 32, leftPos + X_SIZE + COVER_SIZE + 32,
topPos - COVER_SIZE, topPos + Y_SIZE + COVER_SIZE,
printout, page, new Matrix3x2f(graphics.pose()), graphics.scissorStack.peek()
));
}
@Override
@@ -146,4 +151,56 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
return new PrintoutInfo(pages, book, text, colours);
}
}
public record PrintoutRenderState(
int x0, int x1, int y0, int y1, PrintoutInfo printout, int page, Matrix3x2f pose,
@Nullable ScreenRectangle scissorArea, @Nullable ScreenRectangle bounds
) implements PictureInPictureRenderState {
private PrintoutRenderState(
int x0, int x1, int y0, int y1, PrintoutInfo printout, int page, Matrix3x2f pose, @Nullable ScreenRectangle scissorArea
) {
this(x0, x1, y0, y1, printout, page, pose, scissorArea, PictureInPictureRenderState.getBounds(x0, x1, y0, y1, scissorArea));
}
@Override
public float scale() {
return 1.0f;
}
}
/**
* PIP renderer for printouts.
* <p>
* We prefer using a PIP (rather than a {@link GuiElementRenderState}), as {@link PrintoutRenderer} renders with
* multiple z-levels.
*/
public static final class PrintoutPictureRenderer extends PictureInPictureRenderer<PrintoutRenderState> {
public PrintoutPictureRenderer(MultiBufferSource.BufferSource bufferSource) {
super(bufferSource);
}
@Override
protected void renderToTexture(PrintoutRenderState state, PoseStack pose) {
pose.pushPose();
pose.translate(-0.5f * X_SIZE, -(Y_SIZE + COVER_SIZE), 0);
pose.scale(1.0f, 1.0f, -1.0f);
drawBorder(pose, bufferSource, 0, 0, 0, state.page(), state.printout().pages(), state.printout().book(), LightTexture.FULL_BRIGHT);
drawText(
pose, bufferSource, X_TEXT_MARGIN, Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * state.page(), LightTexture.FULL_BRIGHT,
state.printout().text(), state.printout().colour()
);
pose.popPose();
}
@Override
public Class<PrintoutRenderState> getRenderStateClass() {
return PrintoutRenderState.class;
}
@Override
protected String getTextureLabel() {
return "Printout";
}
}
}

View File

@@ -7,12 +7,12 @@ 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.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;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@@ -50,7 +50,7 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
var advanced = family == ComputerFamily.ADVANCED;
graphics.blit(
RenderType::guiTextured, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL,
RenderPipelines.GUI_TEXTURED, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL,
leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0,
TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE
);
@@ -61,14 +61,15 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
var slotX = slot % 4;
var slotY = slot / 4;
graphics.blitSprite(
RenderType::guiTextured, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
RenderPipelines.GUI_TEXTURED, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 22, 22
);
}
// Render sidebar
SpriteRenderer.inGui(graphics, spriteRenderer ->
ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset)
graphics.blitSprite(
RenderPipelines.GUI_TEXTURED, 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

@@ -9,7 +9,7 @@ import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
@@ -48,7 +48,7 @@ public class DynamicImageButton extends Button {
setTooltip(message.tooltip());
var texture = this.texture.get(isHoveredOrFocused());
graphics.blitSprite(RenderType::guiTextured, texture, getX(), getY(), width, height);
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, texture, getX(), getY(), width, height);
}
public record HintedMessage(Component message, Tooltip tooltip) {

View File

@@ -4,6 +4,9 @@
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.gui.KeyConverter;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
@@ -13,8 +16,16 @@ import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.narration.NarratedElementType;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.TextureSetup;
import net.minecraft.client.gui.render.state.GuiElementRenderState;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import org.joml.Matrix3x2f;
import org.joml.Matrix4f;
import org.jspecify.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.util.BitSet;
@@ -82,7 +93,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;
}
@@ -118,7 +129,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;
@@ -253,12 +264,23 @@ public class TerminalWidget extends AbstractWidget {
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
if (!visible) return;
graphics.drawSpecial(bufferSource -> {
FixedWidthFontRenderer.drawTerminal(
FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
(float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
);
});
var scissor = graphics.scissorStack.peek();
var terminalPose = new Matrix3x2f(graphics.pose());
var terminalTextures = TextureSetup.singleTextureWithLightmap(graphics.minecraft.getTextureManager().getTexture(FixedWidthFontRenderer.FONT).getTextureView());
graphics.guiRenderState.submitGuiElement(new TerminalBackgroundRenderState(
innerX, innerY, terminal, terminalPose, terminalTextures,
maybeIntersect(scissor, new ScreenRectangle(
innerX, innerY, terminal.getWidth() * FONT_WIDTH, terminal.getHeight() * FONT_HEIGHT).transformMaxBounds(graphics.pose())
),
scissor
));
graphics.guiRenderState.submitGuiElement(new TerminalTextRenderState(
innerX, innerY, terminal, terminalPose, terminalTextures,
maybeIntersect(scissor, new ScreenRectangle(getX(), getY(), getWidth(), getHeight()).transformMaxBounds(graphics.pose())),
scissor
));
}
@Override
@@ -273,4 +295,49 @@ public class TerminalWidget extends AbstractWidget {
public static int getHeight(int termHeight) {
return termHeight * FONT_HEIGHT + MARGIN * 2;
}
private static @Nullable ScreenRectangle maybeIntersect(@Nullable ScreenRectangle scissor, ScreenRectangle bounds) {
return scissor == null ? bounds : bounds.intersection(scissor);
}
private record TerminalBackgroundRenderState(
int x, int y, Terminal terminal,
Matrix3x2f pose,
TextureSetup textureSetup,
@Nullable ScreenRectangle bounds,
@Nullable ScreenRectangle scissorArea
) implements GuiElementRenderState {
@Override
public void buildVertices(VertexConsumer vertexConsumer, float z) {
var quads = new FixedWidthFontRenderer.QuadEmitter(new Matrix4f().mul(pose).translate(0, 0, z), vertexConsumer);
FixedWidthFontRenderer.drawTerminalBackground(quads, x, y, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
}
@Override
public RenderPipeline pipeline() {
return RenderPipelines.TEXT;
}
}
private record TerminalTextRenderState(
int x, int y, Terminal terminal, Matrix3x2f pose, TextureSetup textureSetup,
@Nullable ScreenRectangle bounds, @Nullable ScreenRectangle scissorArea
) implements GuiElementRenderState {
@Override
public void buildVertices(VertexConsumer vertexConsumer, float z) {
var quads = new FixedWidthFontRenderer.QuadEmitter(new Matrix4f().mul(pose).translate(0, 0, z), vertexConsumer);
FixedWidthFontRenderer.drawTerminalForeground(quads, x, y, terminal);
FixedWidthFontRenderer.drawCursor(quads, x, y, terminal);
// The GUI renderer requires that the buffer is non-empty. Add a zero-size vertex so we always have something.
for (var i = 0; i < 4; i++) {
vertexConsumer.addVertex(0, 0, z).setColor(0x00ffffff).setUv(0, 0).setLight(LightTexture.FULL_BRIGHT);
}
}
@Override
public RenderPipeline pipeline() {
return RenderPipelines.TEXT;
}
}
}

View File

@@ -5,18 +5,16 @@
package dan200.computercraft.client.integration;
import com.google.auto.service.AutoService;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.irisshaders.iris.api.v0.IrisApi;
import net.irisshaders.iris.api.v0.IrisTextVertexSink;
import net.minecraft.client.renderer.LightTexture;
import org.lwjgl.system.MemoryUtil;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.function.IntFunction;
@AutoService(ShaderMod.Provider.class)
public class IrisShaderMod implements ShaderMod.Provider {
@@ -32,21 +30,20 @@ public class IrisShaderMod implements ShaderMod.Provider {
}
@Override
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, ByteBufferBuilder makeBuffer) {
return IrisApi.getInstance().getMinorApiRevision() >= 1
? new IrisQuadEmitter(vertexCount, makeBuffer)
: super.getQuadEmitter(vertexCount, makeBuffer);
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
return new IrisQuadEmitter(quadCount, makeBuffer);
}
private static final class IrisQuadEmitter implements DirectFixedWidthFontRenderer.QuadEmitter {
private static final class IrisQuadEmitter extends DirectFixedWidthFontRenderer.QuadEmitter {
private final IrisTextVertexSink sink;
private @Nullable ByteBuffer buffer;
private IrisQuadEmitter(int vertexCount, ByteBufferBuilder builder) {
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, i -> {
if (buffer != null) throw new IllegalStateException("Allocated multiple buffers");
return buffer = MemoryUtil.memByteBuffer(builder.reserve(i), i);
});
private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> builder) {
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, builder);
}
@Override
public ByteBuffer byteBuffer() {
return sink.getUnderlyingByteBuffer();
}
@Override

View File

@@ -4,12 +4,14 @@
package dan200.computercraft.client.integration;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.VertexBuffer;
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.IntFunction;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.TERMINAL_TEXT;
/**
* Find the currently loaded shader mod (if present) and provides utilities for interacting with it.
@@ -29,14 +31,14 @@ public class ShaderMod {
}
/**
* Get an appropriate quad emitter for use with {@link VertexBuffer} and {@link DirectFixedWidthFontRenderer} .
* Get an appropriate quad emitter for use with a vertex buffer and {@link DirectFixedWidthFontRenderer} .
*
* @param vertexCount The number of vertices.
* @param buffer A function to allocate a temporary buffer.
* @param quadCount The number of quads.
* @param makeBuffer A function to allocate a temporary buffer.
* @return The quad emitter.
*/
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, ByteBufferBuilder buffer) {
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(buffer);
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(makeBuffer.apply(TERMINAL_TEXT.format().getVertexSize() * quadCount * 4));
}
public interface Provider {

View File

@@ -8,13 +8,14 @@ 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.pocket.core.PocketSide;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import mezz.jei.api.IModPlugin;
import mezz.jei.api.JeiPlugin;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.ingredients.subtypes.IIngredientSubtypeInterpreter;
import mezz.jei.api.ingredients.subtypes.ISubtypeInterpreter;
import mezz.jei.api.registration.IAdvancedRegistration;
import mezz.jei.api.registration.ISubtypeRegistration;
import mezz.jei.api.runtime.IJeiRuntime;
@@ -46,7 +47,7 @@ public class JEIComputerCraft implements IModPlugin {
@Override
public void registerAdvanced(IAdvancedRegistration registry) {
registry.addRecipeManagerPlugin(new RecipeResolver(getRegistryAccess()));
registry.addSimpleRecipeManagerPlugin(RecipeTypes.CRAFTING, new RecipeResolver(getRegistryAccess()));
}
@Override
@@ -62,7 +63,7 @@ public class JEIComputerCraft implements IModPlugin {
// Hide all upgrade recipes
var category = registry.createRecipeLookup(RecipeTypes.CRAFTING);
category.get().forEach(wrapper -> {
if (RecipeModHelpers.shouldRemoveRecipe(wrapper.id())) {
if (RecipeModHelpers.shouldRemoveRecipe(wrapper.id().location())) {
registry.hideRecipes(RecipeTypes.CRAFTING, List.of(wrapper));
}
});
@@ -71,7 +72,7 @@ public class JEIComputerCraft implements IModPlugin {
/**
* Distinguishes turtles by upgrades and family.
*/
private static final IIngredientSubtypeInterpreter<ItemStack> turtleSubtype = (stack, ctx) -> {
private static final ISubtypeInterpreter<ItemStack> turtleSubtype = (stack, ctx) -> {
var name = new StringBuilder("turtle:");
// Add left and right upgrades to the identifier
@@ -87,12 +88,15 @@ public class JEIComputerCraft implements IModPlugin {
/**
* Distinguishes pocket computers by upgrade and family.
*/
private static final IIngredientSubtypeInterpreter<ItemStack> pocketSubtype = (stack, ctx) -> {
private static final ISubtypeInterpreter<ItemStack> pocketSubtype = (stack, ctx) -> {
var name = new StringBuilder("pocket:");
// Add the upgrade to the identifier
var upgrade = PocketComputerItem.getUpgradeWithData(stack);
if (upgrade != null) name.append(upgrade.holder().key().location());
var back = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BACK);
var bottom = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BOTTOM);
if (back != null) name.append(back.holder().key().location());
if (back != null && bottom != null) name.append('|');
if (bottom != null) name.append(bottom.holder().key().location());
return name.toString();
};
@@ -100,7 +104,7 @@ public class JEIComputerCraft implements IModPlugin {
/**
* Distinguishes disks by colour.
*/
private static final IIngredientSubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DyedItemColor.getOrDefault(stack, -1));
private static final ISubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DyedItemColor.getOrDefault(stack, -1));
private static RegistryAccess getRegistryAccess() {
return Minecraft.getInstance().level.registryAccess();

View File

@@ -8,71 +8,90 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.integration.UpgradeRecipeGenerator;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.recipe.IFocus;
import mezz.jei.api.recipe.RecipeType;
import mezz.jei.api.recipe.advanced.IRecipeManagerPlugin;
import mezz.jei.api.recipe.category.IRecipeCategory;
import mezz.jei.api.ingredients.ITypedIngredient;
import mezz.jei.api.recipe.advanced.ISimpleRecipeManagerPlugin;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.*;
import net.minecraft.world.item.crafting.display.RecipeDisplay;
import net.minecraft.world.level.Level;
import java.util.List;
class RecipeResolver implements IRecipeManagerPlugin {
private final UpgradeRecipeGenerator<RecipeHolder<CraftingRecipe>> resolver;
class RecipeResolver implements ISimpleRecipeManagerPlugin<RecipeHolder<CraftingRecipe>> {
/**
* We need to generate unique ids for each recipe, as JEI will attempt to deduplicate them otherwise.
*/
private int nextId = 0;
private final UpgradeRecipeGenerator<RecipeHolder<CraftingRecipe>> resolver;
RecipeResolver(HolderLookup.Provider registries) {
resolver = new UpgradeRecipeGenerator<>(
x -> new RecipeHolder<>(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "upgrade_" + nextId++), x),
registries
);
resolver = new UpgradeRecipeGenerator<>(x -> new RecipeHolder<>(
ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "upgrade_" + nextId++)),
new CraftingWrapper(x)
), registries);
}
@Override
public <V> List<RecipeType<?>> getRecipeTypes(IFocus<V> focus) {
var value = focus.getTypedValue().getIngredient();
if (!(value instanceof ItemStack stack)) return List.of();
return switch (focus.getRole()) {
case INPUT ->
stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem || resolver.isUpgrade(stack)
? List.of(RecipeTypes.CRAFTING)
: List.of();
case OUTPUT -> stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem
? List.of(RecipeTypes.CRAFTING)
: List.of();
default -> List.of();
};
public boolean isHandledInput(ITypedIngredient<?> input) {
return input.getIngredient() instanceof ItemStack stack
&& (stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem || resolver.isUpgrade(stack));
}
@Override
public <T, V> List<T> getRecipes(IRecipeCategory<T> recipeCategory, IFocus<V> focus) {
if (!(focus.getTypedValue().getIngredient() instanceof ItemStack stack) || recipeCategory.getRecipeType() != RecipeTypes.CRAFTING) {
return List.of();
}
return switch (focus.getRole()) {
case INPUT -> cast(RecipeTypes.CRAFTING, resolver.findRecipesWithInput(stack));
case OUTPUT -> cast(RecipeTypes.CRAFTING, resolver.findRecipesWithOutput(stack));
default -> List.of();
};
public boolean isHandledOutput(ITypedIngredient<?> output) {
return output.getIngredient() instanceof ItemStack stack
&& (stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem);
}
@Override
public <T> List<T> getRecipes(IRecipeCategory<T> recipeCategory) {
public List<RecipeHolder<CraftingRecipe>> getRecipesForInput(ITypedIngredient<?> input) {
return input.getIngredient() instanceof ItemStack stack ? resolver.findRecipesWithInput(stack) : List.of();
}
@Override
public List<RecipeHolder<CraftingRecipe>> getRecipesForOutput(ITypedIngredient<?> output) {
return output.getIngredient() instanceof ItemStack stack ? resolver.findRecipesWithOutput(stack) : List.of();
}
@Override
public List<RecipeHolder<CraftingRecipe>> getAllRecipes() {
return List.of();
}
@SuppressWarnings({ "unchecked", "rawtypes", "UnusedVariable" })
private static <T, U> List<T> cast(RecipeType<U> ignoredType, List<U> from) {
return (List) from;
private record CraftingWrapper(RecipeDisplay recipes) implements CraftingRecipe {
@Override
public RecipeSerializer<? extends CraftingRecipe> getSerializer() {
throw new IllegalStateException("Should not serialise CraftingWrapper");
}
@Override
public CraftingBookCategory category() {
return CraftingBookCategory.MISC;
}
@Override
public boolean matches(CraftingInput input, Level level) {
return false;
}
@Override
public ItemStack assemble(CraftingInput input, HolderLookup.Provider registries) {
return ItemStack.EMPTY;
}
@Override
public PlacementInfo placementInfo() {
return PlacementInfo.NOT_PLACEABLE;
}
@Override
public List<RecipeDisplay> display() {
return List.of(recipes);
}
}
}

View File

@@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.item.model;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.block.model.ItemTransforms;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.DelegateBakedModel;
/**
* A {@link BakedModel} that wraps another model, but providing different {@link ItemTransforms}.
*/
class BakedModelWithTransform extends DelegateBakedModel {
private final ItemTransforms transforms;
BakedModelWithTransform(BakedModel bakedModel, ItemTransforms transforms) {
super(bakedModel);
this.transforms = transforms;
}
static void addLayer(ItemStackRenderState state, BakedModel model, ItemTransforms transforms) {
state.newLayer().setupBlockModel(new BakedModelWithTransform(model, transforms), Sheets.translucentItemSheet());
}
@Override
public ItemTransforms getTransforms() {
return transforms;
}
}

View File

@@ -7,8 +7,8 @@ package dan200.computercraft.client.item.model;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
@@ -39,8 +39,12 @@ public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel
var overlay = TurtleItem.getOverlay(stack);
if (overlay == null) return;
var model = ClientPlatformHelper.get().getModel(Minecraft.getInstance().getModelManager(), overlay.model());
BakedModelWithTransform.addLayer(state, model, transforms());
state.appendModelIdentityElement(this);
state.appendModelIdentityElement(overlay);
var layer = state.newLayer();
TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), overlay).model().setupItemLayer(layer);
layer.setTransform(transforms().getTransform(context));
}
public record Unbaked(ResourceLocation base) implements ItemModel.Unbaked {
@@ -51,7 +55,7 @@ public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel
@Override
public ItemModel bake(BakingContext bakingContext) {
return new TurtleOverlayModel(bakingContext.bake(base).getTransforms());
return new TurtleOverlayModel(bakingContext.blockModelBaker().getModel(base).getTopTransforms());
}
@Override

View File

@@ -4,23 +4,18 @@
package dan200.computercraft.client.item.model;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Transformation;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransforms;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.special.SpecialModelRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
@@ -28,12 +23,12 @@ import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* An {@link ItemModel} that renders a turtle upgrade, using its {@link TurtleUpgradeModeller}.
* An {@link ItemModel} that renders a turtle upgrade, using its {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModel}.
*
* @param side The side the upgrade resides on.
* @param base The base model. Only used to provide item transforms.
*/
public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements ItemModel {
public record TurtleUpgradeModel(TurtleSide side, ItemTransforms base) implements ItemModel {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/upgrade");
public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
TurtleSide.CODEC.fieldOf("side").forGetter(Unbaked::side),
@@ -41,21 +36,12 @@ public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements It
).apply(instance, Unbaked::new));
@Override
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int light) {
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int seed) {
var upgrade = TurtleItem.getUpgradeWithData(stack, side);
if (upgrade == null) return;
switch (TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side)) {
case TransformedModel.Item model -> {
var childState = new ItemStackRenderState();
resolver.updateForTopItem(childState, model.stack(), ItemDisplayContext.NONE, false, level, null, 0);
if (!childState.isEmpty()) {
state.newLayer().setupSpecialModel(new TransformedRenderer(childState, model.transformation()), null, base);
}
}
case TransformedModel.Baked baked ->
BakedModelWithTransform.addLayer(state, baked.model(), base.getTransforms());
}
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
.renderForItem(upgrade, side, state, resolver, base.getTransform(context), seed);
}
public record Unbaked(TurtleSide side, ResourceLocation base) implements ItemModel.Unbaked {
@@ -66,29 +52,12 @@ public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements It
@Override
public ItemModel bake(BakingContext bakingContext) {
return new TurtleUpgradeModel(side, bakingContext.bake(base));
return new TurtleUpgradeModel(side, bakingContext.blockModelBaker().getModel(base).getTopTransforms());
}
@Override
public void resolveDependencies(Resolver resolver) {
resolver.resolve(base);
}
}
private record TransformedRenderer(
ItemStackRenderState state, Transformation transform
) implements SpecialModelRenderer<Void> {
@Override
public void render(@Nullable Void object, ItemDisplayContext itemDisplayContext, PoseStack poseStack, MultiBufferSource multiBufferSource, int overlay, int light, boolean bl) {
poseStack.pushPose();
poseStack.mulPose(transform.getMatrix());
state.render(poseStack, multiBufferSource, overlay, light);
poseStack.popPose();
}
@Override
public @Nullable Void extractArgument(ItemStack itemStack) {
return null;
resolver.markDependency(base);
}
}
}

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.client.item.properties;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.pocket.ClientPocketComputers;
@@ -38,6 +39,11 @@ public final class PocketComputerStateProperty implements SelectItemModelPropert
return computer == null ? ComputerState.OFF : computer.getState();
}
@Override
public Codec<ComputerState> valueCodec() {
return ComputerState.CODEC;
}
@Override
public Type<? extends SelectItemModelProperty<ComputerState>, ComputerState> type() {
return TYPE;

View File

@@ -6,8 +6,10 @@ package dan200.computercraft.client.item.properties;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
import net.minecraft.resources.ResourceLocation;
@@ -29,7 +31,7 @@ public class TurtleShowElfOverlay implements ConditionalItemModelProperty {
@Override
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
var overlay = TurtleItem.getOverlay(stack);
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
return overlay == null || overlay.showElfOverlay();
}

View File

@@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
/**
* A list of extra models to load on the client.
* <p>
* This is largely intended for use with {@linkplain TurtleOverlay turtle overlays}. As overlays are stored in a dynamic
* registry, they are not available when resources are loaded, and so we need a way to request the overlays' models be
* loaded.
*
* @param models The models to load.
*/
public record ExtraModels(List<ResourceLocation> models) {
private static final Logger LOG = LoggerFactory.getLogger(ExtraModels.class);
private static final Gson GSON = new Gson();
/**
* The path where the extra models are listed.
*/
public static final ResourceLocation PATH = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "extra_models.json");
/**
* The coded used to store the extra model file.
*/
public static final Codec<ExtraModels> CODEC = ResourceLocation.CODEC.listOf().xmap(ExtraModels::new, ExtraModels::models);
/**
* Get the list of all extra models to load.
*
* @param resources The current resource manager.
* @return A set of all resources to load.
*/
public static Collection<ResourceLocation> loadAll(ResourceManager resources) {
Set<ResourceLocation> out = new HashSet<>();
for (var path : resources.getResourceStack(PATH)) {
ExtraModels models;
try (var stream = path.openAsReader()) {
models = ExtraModels.CODEC.parse(JsonOps.INSTANCE, GSON.fromJson(stream, JsonElement.class)).getOrThrow(JsonParseException::new);
} catch (IOException | RuntimeException e) {
LOG.error("Failed to load extra models from {}", path.sourcePackId());
continue;
}
out.addAll(models.models());
}
return Collections.unmodifiableCollection(out);
}
}

View File

@@ -4,7 +4,6 @@
package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import dan200.computercraft.client.ClientTableFormatter;
import dan200.computercraft.client.gui.AbstractComputerScreen;
import dan200.computercraft.client.gui.OptionScreen;
@@ -36,7 +35,6 @@ import java.util.UUID;
/**
* The client-side implementation of {@link ClientNetworkContext}.
*/
@AutoService(ClientNetworkContext.class)
public final class ClientNetworkContextImpl implements ClientNetworkContext {
@Override
public void handleChatTable(TableBuilder table) {

View File

@@ -4,25 +4,38 @@
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 dan200.computercraft.impl.Services;
import net.minecraft.client.resources.model.ModelDebugName;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
public interface ClientPlatformHelper {
static ClientPlatformHelper get() {
return (ClientPlatformHelper) dan200.computercraft.impl.client.ClientPlatformHelper.get();
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
}
/**
* Render a {@link BakedModel}, using any loader-specific hooks.
* Create a new unique {@link ModelKey}.
*
* @param transform The current matrix transformation to apply.
* @param buffers The current pool of render buffers.
* @param model The model to draw.
* @param lightmapCoord The current packed lightmap coordinate.
* @param overlayLight The current overlay light.
* @param tints Block colour tints to apply to the model.
* @param name The debug name for this model key.
* @param <T> The type of baked model.
* @return The newly created model key.
*/
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, int @Nullable [] tints);
@Contract("_ -> new")
<T> ModelKey<T> createModelKey(ModelDebugName name);
final class Instance {
static final @Nullable ClientPlatformHelper INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(ClientPlatformHelper.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
}

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.platform;
import net.minecraft.client.resources.model.ModelManager;
import org.jspecify.annotations.Nullable;
/**
* A key used to identify extra/standalone models.
*
* @param <T> The type of baked model.
*/
public interface ModelKey<T> {
/**
* Lookup this model key in the model manager.
*
* @param manager The model manager.
* @return The loaded model, or {@code null} if not available.
*/
@Nullable
T get(ModelManager manager);
}

View File

@@ -29,7 +29,7 @@ public final class CableHighlightRenderer {
*/
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
var pos = hit.getBlockPos();
var world = camera.getEntity().getCommandSenderWorld();
var world = camera.getEntity().level();
var state = world.getBlockState(pos);

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

@@ -12,13 +12,13 @@ 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;
@@ -39,19 +39,16 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON
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
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) {
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay, Vec3 camera) {
poseStack.pushPose();
poseStack.translate(0.5f, 1.0625f, 0.5f);
poseStack.mulPose(Axis.YP.rotationDegrees(-lectern.getBlockState().getValue(LecternBlock.FACING).getClockWise().toYRot()));
@@ -59,9 +56,9 @@ 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());
@@ -82,7 +79,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
// 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(FixedWidthFontRenderer.TERMINAL_TEXT));
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(camera, POCKET_TERMINAL_RENDER_DISTANCE)) {
renderPocketTerminal(poseStack, quadEmitter, terminal);
} else {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);

View File

@@ -12,8 +12,7 @@ import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Additional render state attached to a {@link ItemFrameRenderState}.

View File

@@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.util.ARGB;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
* Utilities for rendering {@link BakedModel}s and {@link BakedQuad}s.
*/
public final class ModelRenderer {
private ModelRenderer() {
}
/**
* Render a list of {@linkplain BakedQuad quads} to a buffer.
* <p>
* This is not intended to be used directly, but instead by {@link ClientPlatformHelper#renderBakedModel(PoseStack, MultiBufferSource, BakedModel, int, int, int[])}. The
* implementation here is identical to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, int[], int, int)}.
*
* @param transform The current matrix transformation to apply.
* @param buffer The buffer to draw to.
* @param quads The quads to draw.
* @param lightmapCoord The current packed lightmap coordinate.
* @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, int @Nullable [] tints) {
var matrix = transform.last();
for (var bakedquad : quads) {
float r = 1.0f, g = 1.0f, b = 1.0f, a = 1.0f;
if (tints != null && bakedquad.isTinted()) {
var idx = bakedquad.getTintIndex();
if (idx >= 0 && idx < tints.length) {
var tint = tints[bakedquad.getTintIndex()];
r = ARGB.red(tint) / 255.0f;
g = ARGB.green(tint) / 255.0f;
b = ARGB.blue(tint) / 255.0f;
a = ARGB.alpha(tint) / 255.0f;
}
}
buffer.putBulkData(matrix, bakedquad, r, g, b, a, lightmapCoord, overlayLight);
}
}
}

View File

@@ -13,15 +13,17 @@ 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.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
import net.minecraft.util.ARGB;
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;
@@ -31,6 +33,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() {
}
@@ -85,14 +92,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(RenderType.text(GuiSprites.TEXTURE)), 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) {
@@ -103,4 +165,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
ARGB.opaque(colour), LightTexture.FULL_BRIGHT
);
}
private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice(
36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12), false
);
private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice(
36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false
);
private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) {
return scaling instanceof GuiSpriteScaling.NineSlice slice ? slice : fallback;
}
}

View File

@@ -5,134 +5,71 @@
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.gui.GuiSprites;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.LightTexture;
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;
import java.util.function.Consumer;
/**
* 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 void inGui(GuiGraphics graphics, Consumer<SpriteRenderer> renderer) {
graphics.drawSpecial(bufferSource -> renderer.accept(new SpriteRenderer(
graphics.pose().last().pose(), bufferSource.getBuffer(RenderType.guiTextured(GuiSprites.TEXTURE)),
0, LightTexture.FULL_BRIGHT, 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.21.4): 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.21.4): 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

@@ -7,12 +7,12 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.client.ClientRegistry;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
@@ -21,14 +21,12 @@ import net.minecraft.client.renderer.MultiBufferSource;
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.resources.model.BakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ARGB;
import net.minecraft.util.CommonColors;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.Nullable;
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
@@ -45,7 +43,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
}
@Override
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight) {
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, Vec3 camera) {
transform.pushPose();
// Translate the turtle first, so the label moves with it.
@@ -67,8 +65,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();
}
@@ -81,7 +79,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
// Render the turtle
var colour = turtle.getColour();
var overlay = turtle.getOverlay();
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
if (colour == -1) {
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
@@ -91,10 +89,10 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
}
// Render the overlay
if (overlay != null) renderModel(transform, buffers, lightmapCoord, overlayLight, overlay.model(), null);
if (overlay != null) overlay.model().render(transform, buffers, lightmapCoord, overlayLight);
// And the Christmas overlay.
var showChristmas = TurtleOverlay.showElfOverlay(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
var showChristmas = Holiday.getCurrent() == Holiday.CHRISTMAS && (overlay == null || overlay.showElfOverlay());
if (showChristmas) renderModel(transform, buffers, lightmapCoord, overlayLight, TurtleOverlay.ELF_MODEL, null);
// Render the upgrades
@@ -105,7 +103,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
}
private void renderUpgrade(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
var upgrade = turtle.getUpgrade(side);
var upgrade = turtle.getAccess().getUpgradeWithData(side);
if (upgrade == null) return;
transform.pushPose();
@@ -114,40 +112,15 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
transform.translate(0.0f, -0.5f, -0.5f);
switch (TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side)) {
case TransformedModel.Item model -> {
transform.mulPose(model.transformation().getMatrix());
transform.mulPose(Axis.YP.rotation(Mth.PI));
Minecraft.getInstance().getItemRenderer().renderStatic(
model.stack(), ItemDisplayContext.FIXED, lightmapCoord, overlayLight, transform, buffers, turtle.getLevel(), 0
);
}
case TransformedModel.Baked model ->
renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
}
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
.renderForLevel(upgrade, side, turtle.getAccess(), transform, buffers, lightmapCoord, overlayLight);
transform.popPose();
}
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, int @Nullable [] tints) {
var modelManager = Minecraft.getInstance().getModelManager();
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
ClientRegistry.getModel(modelManager, modelLocation).render(transform, buffers, lightmapCoord, overlayLight, tints);
}
/**
* Render a block model.
*
* @param transform The current matrix stack.
* @param renderer The buffer to write to.
* @param lightmapCoord The current lightmap coordinate.
* @param overlayLight The overlay light.
* @param model The model to render.
* @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, int @Nullable [] tints) {
ClientPlatformHelper.get().renderBakedModel(transform, renderer, model, lightmapCoord, overlayLight, tints);
}
}

View File

@@ -4,8 +4,10 @@
package dan200.computercraft.client.render.monitor;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.*;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.client.FrameInfo;
@@ -17,20 +19,26 @@ import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.renderer.CompiledShaderProgram;
import net.minecraft.client.renderer.FogParameters;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.fog.FogRenderer;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.jspecify.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import java.util.function.Consumer;
import java.nio.ByteBuffer;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBlockEntity> {
/**
@@ -39,13 +47,13 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
*/
private static final float MARGIN = (float) (MonitorBlockEntity.RENDER_MARGIN * 1.1);
private static final ByteBufferBuilder backingBufferBuilder = new ByteBufferBuilder(0x4000);
private static @Nullable ByteBuffer backingBuffer;
public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
}
@Override
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight, Vec3 camera) {
// Render from the origin monitor
var originTerminal = monitor.getOriginClientMonitor();
if (originTerminal == null) return;
@@ -122,27 +130,63 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin
) {
var redraw = monitor.pollTerminalChanged();
if (renderState.createBuffer()) redraw = true;
if (renderState.vertexBuffer == null) redraw = true;
var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
if (redraw) {
var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
// Cursor, Foreground, Background+Margin
var maxQuadCount = 1 + (terminal.getWidth() * terminal.getHeight()) + ((terminal.getWidth() + 2) * (terminal.getHeight() + 2));
var maxVertexCount = 4 * maxQuadCount;
var sink = ShaderMod.get().getQuadEmitter(maxQuadCount, MonitorBlockEntityRenderer::getBuffer);
// In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
// a starting and ending offset, and so need to use two buffers instead.
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin);
var vertexCountAfterBackground = sink.vertexCount();
renderToBuffer(backgroundBuffer, size, sink ->
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
var vertexCountAfterForeground = sink.vertexCount();
renderToBuffer(foregroundBuffer, size + 4, sink -> {
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
// If the cursor is visible, we append it to the end of our buffer. When rendering, we can either
// render n or n+1 quads and so toggle the cursor on and off.
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
});
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
var vertexCountAfterCursor = sink.vertexCount();
if (vertexCountAfterCursor > maxVertexCount) {
throw new IllegalStateException("Drew too many vertices. Expected " + maxVertexCount + ", drew " + vertexCountAfterCursor);
}
if (vertexCountAfterCursor != 0) {
renderState.register();
var commandEncoder = RenderSystem.getDevice().createCommandEncoder();
var resultBuffer = sink.byteBuffer().flip();
// Ensure our buffer contains the correct number of vertices.
if (resultBuffer.remaining() != sink.format().getVertexSize() * vertexCountAfterCursor) {
throw new IllegalStateException(String.format(
"Mismatched vertex count. Buffer is %d bytes long, but was expected to be %d (vertex size) * %d (vertex count) = %d bytes.",
resultBuffer.limit(), sink.format().getVertexSize(), vertexCountAfterCursor, sink.format().getVertexSize() * vertexCountAfterCursor
));
}
// Upload the buffer, reallocating if required.
if (renderState.vertexBuffer == null || resultBuffer.remaining() > renderState.vertexBuffer.size()) {
if (renderState.vertexBuffer != null) {
renderState.vertexBuffer.close();
renderState.vertexBuffer = null;
}
renderState.vertexBuffer = RenderSystem.getDevice().createBuffer(
() -> "Monitor at " + monitor.getOrigin().getBlockPos(), GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, resultBuffer
);
} else if (!renderState.vertexBuffer.isClosed()) {
commandEncoder.writeToBuffer(renderState.vertexBuffer.slice(), resultBuffer);
}
}
renderState.vertexCountAfterBackground = vertexCountAfterBackground;
renderState.vertexCountAfterForeground = vertexCountAfterForeground;
renderState.vertexCountAfterCursor = vertexCountAfterCursor;
}
if (renderState.vertexCountAfterCursor == 0) return;
// Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will
// use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the
// normal render distance (~200), and the edges of the monitor fade out due to fog.
@@ -150,71 +194,72 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
// renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
// absurdly high value.
var oldFog = RenderSystem.getShaderFog();
RenderSystem.setShaderFog(FogParameters.NO_FOG);
FixedWidthFontRenderer.TERMINAL_TEXT.setupRenderState();
RenderSystem.setShaderFog(Minecraft.getInstance().gameRenderer.fogRenderer.getBuffer(FogRenderer.FogMode.NONE));
// Compose the existing model view matrix with our transformation matrix.
var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix);
RenderSystem.getModelViewStack().pushMatrix();
RenderSystem.getModelViewStack().mul(matrix);
// Render background geometry
backgroundBuffer.bind();
backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderSystem.getShader());
// Render foreground geometry with glPolygonOffset enabled.
RenderSystem.polygonOffset(-1.0f, -10.0f);
RenderSystem.enablePolygonOffset();
foregroundBuffer.bind();
drawWithShader(renderState, FixedWidthFontRenderer.TERMINAL_TEXT, RenderPipelines.TEXT, 0, renderState.vertexCountAfterBackground);
drawWithShader(
foregroundBuffer, modelView, RenderSystem.getProjectionMatrix(), RenderSystem.getShader(),
// Skip the cursor quad if it is not visible this frame.
FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
? foregroundBuffer.indexCount - FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(4)
: foregroundBuffer.indexCount
renderState, FixedWidthFontRenderer.TERMINAL_TEXT_OFFSET, RenderPipelines.TEXT_POLYGON_OFFSET, renderState.vertexCountAfterBackground,
(
FixedWidthFontRenderer.isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()
? renderState.vertexCountAfterCursor : renderState.vertexCountAfterForeground
) - renderState.vertexCountAfterBackground
);
// Clear state
RenderSystem.polygonOffset(0.0f, -0.0f);
RenderSystem.disablePolygonOffset();
FixedWidthFontRenderer.TERMINAL_TEXT.clearRenderState();
VertexBuffer.unbind();
RenderSystem.getModelViewStack().popMatrix();
RenderSystem.setShaderFog(oldFog);
}
private static void renderToBuffer(VertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
var sink = ShaderMod.get().getQuadEmitter(size, backingBufferBuilder);
draw.accept(sink);
private static void drawWithShader(MonitorRenderState renderState, RenderType renderType, RenderPipeline pipeline, int vertexOffset, int vertexCount) {
if (renderState.vertexBuffer == null) {
throw new IllegalStateException("MonitorRenderState has not been initialised");
}
if (vertexCount == 0) return;
var result = backingBufferBuilder.build();
if (result == null) {
// If we have nothing to draw, just mark it as empty. We'll skip drawing in drawWithShader.
vbo.indexCount = 0;
return;
var transforms = RenderSystem.getDynamicUniforms().writeTransform(
RenderSystem.getModelViewMatrix(),
new Vector4f(1.0F, 1.0F, 1.0F, 1.0F),
RenderSystem.getModelOffset(),
RenderSystem.getTextureMatrix(),
RenderSystem.getShaderLineWidth()
);
renderType.setupRenderState();
var autoStorageBuffer = RenderSystem.getSequentialBuffer(renderType.mode());
var indexCount = FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(vertexCount);
var indexBuffer = autoStorageBuffer.getBuffer(indexCount);
var target = Minecraft.getInstance().getMainRenderTarget();
var colourTarget = RenderSystem.outputColorTextureOverride != null ? RenderSystem.outputColorTextureOverride : target.getColorTextureView();
var depthTarget = target.useDepth
? (RenderSystem.outputDepthTextureOverride != null ? RenderSystem.outputDepthTextureOverride : target.getDepthTextureView())
: null;
try (var renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
() -> "Monitor", colourTarget, OptionalInt.empty(), depthTarget, OptionalDouble.empty()
)) {
renderPass.setPipeline(pipeline);
RenderSystem.bindDefaultUniforms(renderPass);
renderPass.setUniform("DynamicTransforms", transforms);
renderPass.setVertexBuffer(0, renderState.vertexBuffer);
renderPass.setIndexBuffer(indexBuffer, autoStorageBuffer.type());
for (var j = 0; j < 12; j++) {
var gpuTexture = RenderSystem.getShaderTexture(j);
if (gpuTexture != null) renderPass.bindSampler("Sampler" + j, gpuTexture);
}
renderPass.drawIndexed(vertexOffset, 0, indexCount, 1);
}
var buffer = result.byteBuffer();
var vertices = buffer.limit() / sink.format().getVertexSize();
vbo.bind();
vbo.upload(new MeshData(result, new MeshData.DrawState(
sink.format(),
vertices, FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(vertices),
FixedWidthFontRenderer.TERMINAL_TEXT.mode(), VertexFormat.IndexType.least(vertices)
)));
}
private static void drawWithShader(VertexBuffer buffer, Matrix4f modelView, Matrix4f projection, @Nullable CompiledShaderProgram compiledShaderProgram, int indicies) {
var originalIndexCount = buffer.indexCount;
if (originalIndexCount == 0) return;
try {
buffer.indexCount = indicies;
buffer.drawWithShader(modelView, projection, compiledShaderProgram);
} finally {
buffer.indexCount = originalIndexCount;
}
renderType.clearRenderState();
}
@Override
@@ -226,4 +271,14 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
public AABB getRenderBoundingBox(MonitorBlockEntity monitor) {
return monitor.getRenderBoundingBox();
}
private static ByteBuffer getBuffer(int capacity) {
var buffer = backingBuffer;
if (buffer == null || buffer.capacity() < capacity) {
buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
}
buffer.clear();
return buffer;
}
}

View File

@@ -29,7 +29,7 @@ public final class MonitorHighlightRenderer {
// Preserve normal behaviour when crouching.
if (camera.getEntity().isCrouching()) return false;
var world = camera.getEntity().getCommandSenderWorld();
var world = camera.getEntity().level();
var pos = hit.getBlockPos();
if (!(world.getBlockEntity(pos) instanceof MonitorBlockEntity monitor)) return false;

View File

@@ -5,8 +5,7 @@
package dan200.computercraft.client.render.monitor;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.mojang.blaze3d.buffers.BufferUsage;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.buffers.GpuBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import net.minecraft.core.BlockPos;
import org.jspecify.annotations.Nullable;
@@ -21,53 +20,39 @@ import java.util.Set;
* This is automatically cleared by {@link dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity} when the
* entity is unloaded on the client side (see {@link MonitorRenderState#close()}).
*/
public class MonitorRenderState implements ClientMonitor.RenderState {
public final class MonitorRenderState implements ClientMonitor.RenderState {
@GuardedBy("allMonitors")
private static final Set<MonitorRenderState> allMonitors = new HashSet<>();
public long lastRenderFrame = -1;
public @Nullable BlockPos lastRenderPos = null;
long lastRenderFrame = -1;
@Nullable
BlockPos lastRenderPos = null;
public @Nullable VertexBuffer backgroundBuffer;
public @Nullable VertexBuffer foregroundBuffer;
@Nullable
GpuBuffer vertexBuffer;
/**
* Create the appropriate buffer if needed.
*
* @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer,
* or this mode does not require one.
*/
public boolean createBuffer() {
if (backgroundBuffer != null) return false;
int vertexCountAfterBackground;
int vertexCountAfterForeground;
int vertexCountAfterCursor;
deleteBuffers();
backgroundBuffer = new VertexBuffer(BufferUsage.STATIC_WRITE);
foregroundBuffer = new VertexBuffer(BufferUsage.STATIC_WRITE);
addMonitor();
return true;
}
void register() {
if (vertexBuffer != null) return;
private void addMonitor() {
synchronized (allMonitors) {
allMonitors.add(this);
}
}
private void deleteBuffers() {
if (backgroundBuffer != null) {
backgroundBuffer.close();
backgroundBuffer = null;
}
if (foregroundBuffer != null) {
foregroundBuffer.close();
foregroundBuffer = null;
if (vertexBuffer != null) {
vertexBuffer.close();
vertexBuffer = null;
}
}
@Override
public void close() {
if (backgroundBuffer != null) {
if (vertexBuffer != null) {
synchronized (allMonitors) {
allMonitors.remove(this);
}

View File

@@ -4,7 +4,6 @@
package dan200.computercraft.client.render.text;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat;
@@ -15,6 +14,7 @@ import dan200.computercraft.core.util.Colour;
import net.minecraft.util.ARGB;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.*;
@@ -33,7 +33,7 @@ import static org.lwjgl.system.MemoryUtil.*;
* <p>
* Note this is almost an exact copy of {@link FixedWidthFontRenderer}. While the code duplication is unfortunate,
* it is measurably faster than introducing polymorphism into {@link FixedWidthFontRenderer}.
*
* <p>
* <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
* {@link FixedWidthFontRenderer}.
*/
@@ -156,21 +156,37 @@ public final class DirectFixedWidthFontRenderer {
}
}
public static int getVertexCount(Terminal terminal) {
return (terminal.getHeight() + 2) * (terminal.getWidth() + 2) * 2;
}
private static void quad(QuadEmitter buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
buffer.vertexCount += 4;
buffer.quad(x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
}
public interface QuadEmitter {
VertexFormat format();
public abstract static class QuadEmitter {
private int vertexCount;
void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
public abstract ByteBuffer byteBuffer();
public abstract VertexFormat format();
protected abstract void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
public int vertexCount() {
return vertexCount;
}
}
public record ByteBufferEmitter(ByteBufferBuilder builder) implements QuadEmitter {
public static final class ByteBufferEmitter extends QuadEmitter {
private final ByteBuffer buffer;
public ByteBufferEmitter(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public ByteBuffer byteBuffer() {
return buffer;
}
@Override
public VertexFormat format() {
return TERMINAL_TEXT.format();
@@ -178,17 +194,18 @@ public final class DirectFixedWidthFontRenderer {
@Override
public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
DirectFixedWidthFontRenderer.quad(builder, x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
DirectFixedWidthFontRenderer.quad(buffer, x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
}
}
static void quad(ByteBufferBuilder builder, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
private static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
// Emit a single quad to our buffer. This uses Unsafe (well, LWJGL's MemoryUtil) to directly blit bytes to the
// underlying buffer. This allows us to have a single bounds check up-front, rather than one for every write.
// This provides significant performance gains, at the cost of well, using Unsafe.
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(abgr:BBBB)(uv1:FF)(uv2:SS),
// which matches the POSITION_COLOR_TEX_LIGHTMAP vertex format.
var addr = builder.reserve(112);
var position = buffer.position();
var addr = MemoryUtil.memAddress(buffer);
// We're doing terrible unsafe hacks below, so let's be really sure that what we're doing is reasonable.
// Require the pointer to be aligned to a 32-bit boundary.
@@ -237,6 +254,9 @@ public final class DirectFixedWidthFontRenderer {
memPutShort(addr + 108, (short) 0xF0);
memPutShort(addr + 110, (short) 0xF0);
// Finally increment the position.
buffer.position(position + 112);
// Well done for getting to the end of this method. I recommend you take a break and go look at cute puppies.
}
}

View File

@@ -33,13 +33,15 @@ import org.joml.Vector3f;
* {@link DirectFixedWidthFontRenderer}.
*/
public final class FixedWidthFontRenderer {
private static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
public static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
/**
* A render type for terminal text.
*/
public static final RenderType TERMINAL_TEXT = RenderType.text(FONT);
public static final RenderType TERMINAL_TEXT_OFFSET = RenderType.textPolygonOffset(FONT);
public static final int FONT_HEIGHT = 9;
public static final int FONT_WIDTH = 6;
static final float WIDTH = 256.0f;

View File

@@ -48,7 +48,7 @@ public class SpeakerInstance {
// 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());
channel.linearAttenuation(Math.max(volume, 1) * Nullability.assertNonNull(sound.getSound()).getAttenuationDistance());
}
});
}

View File

@@ -1,60 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.client.turtle;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
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.jspecify.annotations.Nullable;
import java.util.stream.Stream;
/**
* A {@link TurtleUpgradeModeller} for modems, providing different models depending on if the modem is on/off.
*/
public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
@Override
public TransformedModel getModel(TurtleModem upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
var active = DataComponentUtil.isPresent(data, ModRegistry.DataComponents.ON.get(), x -> x);
var models = upgrade.advanced() ? ModemModels.ADVANCED : ModemModels.NORMAL;
return side == TurtleSide.LEFT
? TransformedModel.of(active ? models.leftOnModel() : models.leftOffModel())
: TransformedModel.of(active ? models.rightOnModel() : models.rightOffModel());
}
@Override
public Stream<ResourceLocation> getDependencies() {
return Stream.of(ModemModels.NORMAL, ModemModels.ADVANCED).flatMap(ModemModels::getDependencies);
}
private record ModemModels(
ResourceLocation leftOffModel, ResourceLocation rightOffModel,
ResourceLocation leftOnModel, ResourceLocation rightOnModel
) {
private static final ModemModels NORMAL = create("normal");
private static final ModemModels ADVANCED = create("advanced");
public static ModemModels create(String type) {
return new ModemModels(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right")
);
}
public Stream<ResourceLocation> getDependencies() {
return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel);
}
}
}

View File

@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.resources.ResourceLocation;
/**
* A cosmetic overlay on a turtle.
*
* @param model The path to the overlay's model.
* @param showElfOverlay Whether this overlay is compatible with the {@linkplain #ELF_MODEL Christmas elf model}.
* @see ModRegistry.DataComponents#OVERLAY
*/
public record TurtleOverlay(StandaloneModel model, boolean showElfOverlay) {
/**
* The folder where upgrades are loaded from.
*/
public static final String SOURCE = ComputerCraftAPI.MOD_ID + "/turtle_overlay";
/**
* The codec used to read/write turtle overlay definitions from resource packs.
*/
public static final Codec<TurtleOverlay.Unbaked> CODEC = RecordCodecBuilder.create(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("model").forGetter(TurtleOverlay.Unbaked::model),
Codec.BOOL.optionalFieldOf("show_elf_overlay", false).forGetter(TurtleOverlay.Unbaked::showElfOverlay)
).apply(instance, TurtleOverlay.Unbaked::new));
/**
* An additional overlay that is rendered on all turtles at {@linkplain Holiday#CHRISTMAS Christmas}.
*
* @see #showElfOverlay()
*/
public static final ResourceLocation ELF_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
public record Unbaked(ResourceLocation model, boolean showElfOverlay) implements ResolvableModel {
@Override
public void resolveDependencies(Resolver resolver) {
resolver.markDependency(model());
}
public TurtleOverlay bake(ModelBaker baker) {
return new TurtleOverlay(StandaloneModel.of(model(), baker), showElfOverlay());
}
}
}

View File

@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import dan200.computercraft.client.CustomModelManager;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
/**
* The model manager for {@link TurtleOverlay}s.
*/
public class TurtleOverlayManager {
private static final CustomModelManager<TurtleOverlay.Unbaked, TurtleOverlay> loader = new CustomModelManager<>(
"turtle overlay", FileToIdConverter.json(TurtleOverlay.SOURCE), TurtleOverlay.CODEC,
TurtleOverlay.Unbaked::bake,
new TurtleOverlay.Unbaked(MissingBlockModel.LOCATION, false)
);
public static CustomModelManager<TurtleOverlay.Unbaked, TurtleOverlay> loader() {
return loader;
}
/**
* Find the turtle overlay with the given id. If the overlay does not exist, then the "missing model" overlay is
* returned instead.
*
* @param modelManager The model manager.
* @param id The overlay id.
* @return The turtle overlay.
*/
@Contract("_, null -> null; _, !null -> !null")
public static @Nullable TurtleOverlay get(ModelManager modelManager, @Nullable ResourceLocation id) {
return id == null ? null : loader.get(modelManager, id);
}
}

View File

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import dan200.computercraft.api.client.turtle.BasicUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.CustomModelManager;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.core.Holder;
import net.minecraft.resources.FileToIdConverter;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
/**
* The model manager for {@link TurtleUpgradeModel}s.
*/
public final class TurtleUpgradeModelManager {
private static final CustomModelManager<TurtleUpgradeModel.Unbaked, TurtleUpgradeModel> loader = new CustomModelManager<>(
"turtle upgrade", FileToIdConverter.json(TurtleUpgradeModel.SOURCE), TurtleUpgradeModel.CODEC,
TurtleUpgradeModel.Unbaked::bake,
BasicUpgradeModel.unbaked(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION)
);
public static CustomModelManager<TurtleUpgradeModel.Unbaked, TurtleUpgradeModel> loader() {
return loader;
}
/**
* Find the model for the given turtle upgrade.
*
* @param modelManager The model manager.
* @param upgrade The turtle upgrade
* @return The turtle upgrade model.
*/
@Contract("_, null -> null; _, !null -> !null")
public static @Nullable TurtleUpgradeModel get(ModelManager modelManager, Holder.@Nullable Reference<ITurtleUpgrade> upgrade) {
return upgrade == null ? null : loader.get(modelManager, upgrade.key().location());
}
}

View File

@@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeType;
import dan200.computercraft.shared.util.RegistryHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/**
* A registry of {@link TurtleUpgradeModeller}s.
*/
public final class TurtleUpgradeModellers {
private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side, data) ->
TransformedModel.of(Minecraft.getInstance().getModelManager().getMissingModel());
private static final Map<UpgradeType<? extends ITurtleUpgrade>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
private static volatile boolean fetchedModels;
private TurtleUpgradeModellers() {
}
public static <T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModeller<T> modeller) {
if (fetchedModels) {
throw new IllegalStateException(String.format(
"Turtle upgrade type %s must be registered before models are baked.",
RegistryHelper.getKeyOrThrow(RegistryHelper.getRegistry(ITurtleUpgrade.typeRegistry()), type)
));
}
if (turtleModels.putIfAbsent(type, modeller) != null) {
throw new IllegalStateException("Modeller already registered for serialiser");
}
}
public static TransformedModel getModel(ITurtleUpgrade upgrade, ITurtleAccess access, TurtleSide side) {
return getModeller(upgrade).getModel(upgrade, access, side, access.getUpgradeData(side));
}
public static TransformedModel getModel(ITurtleUpgrade upgrade, DataComponentPatch data, TurtleSide side) {
return getModeller(upgrade).getModel(upgrade, null, side, data);
}
@SuppressWarnings("unchecked")
private static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> getModeller(T upgrade) {
var modeller = turtleModels.get(upgrade.getType());
return (TurtleUpgradeModeller<T>) (modeller == null ? NULL_TURTLE_MODELLER : modeller);
}
public static Stream<ResourceLocation> getDependencies() {
fetchedModels = true;
return turtleModels.values().stream().flatMap(TurtleUpgradeModeller::getDependencies);
}
}

View File

@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.client;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.ClientHooks;
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
/**
* Provides custom block breaking progress for modems, so it only applies to the current part.
*
* @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer)
*/
@Mixin(BlockRenderDispatcher.class)
public class BlockRenderDispatcherMixin {
@ModifyVariable(method = "renderBreakingTexture", at = @At("HEAD"))
public BlockState renderBlockDamage(BlockState state, @Local BlockPos pos) {
return ClientHooks.getBlockBreakingState(state, pos);
}
}

View File

@@ -7,7 +7,6 @@
"defaultRequire": 1
},
"client": [
"BlockRenderDispatcherMixin",
"ClientPacketListenerMixin"
"BlockRenderDispatcherMixin"
]
}

View File

@@ -5,15 +5,15 @@
package dan200.computercraft.data;
import com.mojang.serialization.Codec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
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.client.turtle.TurtleOverlay;
import dan200.computercraft.data.client.BlockModelProvider;
import dan200.computercraft.data.client.ExtraModelsProvider;
import dan200.computercraft.data.client.ItemModelProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.Util;
import net.minecraft.client.data.models.BlockModelGenerators;
@@ -21,6 +21,7 @@ import net.minecraft.client.data.models.ItemModelGenerators;
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.client.resources.model.AtlasIds;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder;
import net.minecraft.data.DataProvider;
@@ -53,35 +54,30 @@ public final class DataProviders {
var fullRegistryPatch = RegistryPatchGenerator.createLookup(
generator.registries(),
Util.make(new RegistrySetBuilder(), builder -> {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::register);
builder.add(IPocketUpgrade.REGISTRY, PocketUpgradeProvider::addUpgrades);
builder.add(TurtleOverlay.REGISTRY, TurtleOverlays::register);
}));
var fullRegistries = fullRegistryPatch.thenApply(RegistrySetBuilder.PatchedRegistries::full);
generator.registries(fullRegistryPatch);
generator.add(out -> new RecipeProvider.Runner(out, fullRegistries));
var blockTags = generator.blockTags(TagProvider::blockTags);
generator.itemTags(TagProvider::itemTags, blockTags);
generator.blockTags(TagProvider::blockTags);
generator.itemTags(TagProvider::itemTags);
generator.add(out -> new net.minecraft.data.loot.LootTableProvider(out, Set.of(), LootTableProvider.getTables(), fullRegistries));
generator.add(out -> new LanguageProvider(out, fullRegistries));
generator.addFromCodec("Block atlases", PackOutput.Target.RESOURCE_PACK, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
out.accept(AtlasIds.BLOCKS, makeSprites(Stream.of(
LecternPrintoutModel.TEXTURE,
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
)));
out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites(Stream.of(
UpgradeSlot.LEFT_UPGRADE,
UpgradeSlot.RIGHT_UPGRADE
)));
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
out.accept(AtlasIds.GUI, makeSprites(
Stream.of(UpgradeSlot.LEFT_UPGRADE, UpgradeSlot.RIGHT_UPGRADE),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
@@ -90,12 +86,10 @@ public final class DataProviders {
));
});
generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) {
@Override
public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
}
});
generator.add(ResourceMetadataProvider::new);
generator.addFromCodec("Turtle overlays", PackOutput.Target.RESOURCE_PACK, TurtleOverlay.SOURCE, TurtleOverlay.CODEC, TurtleOverlays::register);
generator.addFromCodec("Turtle upgrade models", PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC, TurtleUpgradeProvider::addModels);
generator.addModels(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
}
@@ -115,7 +109,7 @@ public final class DataProviders {
TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags);
TagsProvider<Item> itemTags(Consumer<TagProvider.ItemTagConsumer> tags, TagsProvider<Block> blocks);
TagsProvider<Item> itemTags(Consumer<TagProvider.TagConsumer<Item>> tags);
/**
* Build new dynamic registries and save them to a pack.

View File

@@ -100,14 +100,18 @@ public final class LanguageProvider implements DataProvider {
add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), "Pocket Computer");
add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().getDescriptionId() + ".upgraded", "%s Pocket Computer");
add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().getDescriptionId() + ".upgraded_twice", "%s %s Pocket Computer");
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(), "Advanced Pocket Computer");
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded", "Advanced %s Pocket Computer");
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded_twice", "Advanced %s %s Pocket Computer");
// Tags (for EMI)
add(ComputerCraftTags.Items.COMPUTER, "Computers");
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 +191,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

@@ -24,7 +24,6 @@ import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
import dan200.computercraft.shared.recipe.TransformShapedRecipe;
import dan200.computercraft.shared.recipe.TransformShapelessRecipe;
import dan200.computercraft.shared.recipe.function.CopyComponents;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
import dan200.computercraft.shared.util.ColourUtils;
@@ -141,7 +140,7 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
registries.lookupOrThrow(IPocketUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
var upgrade = upgradeHolder.value();
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
.group(name.toString())
.pattern("#")
.pattern("P")
@@ -178,13 +177,11 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
);
}
private void turtleOverlay(ResourceKey<TurtleOverlay> overlay, Consumer<ShapelessSpecBuilder> build) {
var holder = registries.lookupOrThrow(overlay.registryKey()).getOrThrow(overlay);
private void turtleOverlay(ResourceLocation overlay, Consumer<ShapelessSpecBuilder> build) {
for (var turtleItem : turtleItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), holder))
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), overlay))
.group(name.withSuffix("_overlay").toString())
.unlockedBy("has_turtle", has(turtleItem));
build.accept(builder);
@@ -193,7 +190,7 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.build(s -> new TransformShapelessRecipe(s, List.of(
CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
)))
.save(output, name.withSuffix("_overlays/" + overlay.location().getPath()));
.save(output, name.withSuffix("_overlays/" + overlay.getPath()));
}
}

View File

@@ -0,0 +1,113 @@
// 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 com.mojang.serialization.JsonOps;
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;
/**
* Generates {@code .mcmeta} files for texture files.
* <p>
* This is 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), false)
));
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), false)
));
}
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), false)
));
}
}
}
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.name(), () -> type.codec().encodeStart(JsonOps.INSTANCE, value).getOrThrow().getAsJsonObject());
return this;
}
}
}

View File

@@ -7,13 +7,10 @@ package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.integration.ExternalModTags;
import dan200.computercraft.shared.util.RegistryHelper;
import net.minecraft.core.Registry;
import net.minecraft.data.tags.ItemTagsProvider;
import net.minecraft.data.tags.TagAppender;
import net.minecraft.data.tags.TagsProvider;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.ItemTags;
import net.minecraft.tags.TagBuilder;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
@@ -28,14 +25,8 @@ import net.minecraft.world.level.block.Blocks;
*/
class TagProvider {
public static void blockTags(TagConsumer<Block> tags) {
tags.tag(ComputerCraftTags.Blocks.COMPUTER).add(
ModRegistry.Blocks.COMPUTER_NORMAL.get(),
ModRegistry.Blocks.COMPUTER_ADVANCED.get(),
ModRegistry.Blocks.COMPUTER_COMMAND.get()
);
tags.tag(ComputerCraftTags.Blocks.TURTLE).add(ModRegistry.Blocks.TURTLE_NORMAL.get(), ModRegistry.Blocks.TURTLE_ADVANCED.get());
itemAndBlockTags((b, i) -> tags.tag(b));
tags.tag(ComputerCraftTags.Blocks.WIRED_MODEM).add(ModRegistry.Blocks.CABLE.get(), ModRegistry.Blocks.WIRED_MODEM_FULL.get());
tags.tag(ComputerCraftTags.Blocks.MONITOR).add(ModRegistry.Blocks.MONITOR_NORMAL.get(), ModRegistry.Blocks.MONITOR_ADVANCED.get());
tags.tag(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE).addTag(ComputerCraftTags.Blocks.WIRED_MODEM);
@@ -91,11 +82,11 @@ class TagProvider {
);
}
public static void itemTags(ItemTagConsumer tags) {
tags.copy(ComputerCraftTags.Blocks.COMPUTER, ComputerCraftTags.Items.COMPUTER);
tags.copy(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE);
public static void itemTags(TagConsumer<Item> tags) {
itemAndBlockTags((b, i) -> tags.tag(i).map(Block::asItem));
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)
@@ -115,37 +106,32 @@ class TagProvider {
.addTag(ItemTags.BOATS);
}
private static void itemAndBlockTags(BlockItemTagConsumer tags) {
tags.tag(ComputerCraftTags.Blocks.COMPUTER, ComputerCraftTags.Items.COMPUTER).add(
ModRegistry.Blocks.COMPUTER_NORMAL.get(),
ModRegistry.Blocks.COMPUTER_ADVANCED.get(),
ModRegistry.Blocks.COMPUTER_COMMAND.get()
);
tags.tag(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE).add(
ModRegistry.Blocks.TURTLE_NORMAL.get(),
ModRegistry.Blocks.TURTLE_ADVANCED.get()
);
tags.tag(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR).add(
ModRegistry.Blocks.MONITOR_NORMAL.get(),
ModRegistry.Blocks.MONITOR_ADVANCED.get()
);
}
/**
* A wrapper over {@link TagsProvider}.
*
* @param <T> The type of object we're providing tags for.
*/
public interface TagConsumer<T> {
TagAppender<T> tag(TagKey<T> tag);
TagAppender<T, T> tag(TagKey<T> tag);
}
public record TagAppender<T>(Registry<T> registry, TagBuilder builder) {
public TagAppender<T> add(T object) {
builder.addElement(RegistryHelper.getKeyOrThrow(registry, object));
return this;
}
@SafeVarargs
public final TagAppender<T> add(T... objects) {
for (var object : objects) add(object);
return this;
}
public TagAppender<T> addTag(TagKey<T> tag) {
builder.addTag(tag.location());
return this;
}
}
/**
* A wrapper over {@link ItemTagsProvider}.
*/
public interface ItemTagConsumer extends TagConsumer<Item> {
void copy(TagKey<Block> block, TagKey<Item> item);
private interface BlockItemTagConsumer {
TagAppender<Block, ?> tag(TagKey<Block> blockTag, TagKey<Item> itemTag);
}
}

View File

@@ -5,32 +5,32 @@
package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.data.worldgen.BootstrapContext;
import net.minecraft.resources.ResourceKey;
import dan200.computercraft.client.turtle.TurtleOverlay;
import net.minecraft.resources.ResourceLocation;
import java.util.function.BiConsumer;
/**
* Built-in turtle overlays.
*/
final class TurtleOverlays {
public static final ResourceKey<TurtleOverlay> RAINBOW_FLAG = create("rainbow_flag");
public static final ResourceKey<TurtleOverlay> TRANS_FLAG = create("trans_flag");
public static final ResourceLocation RAINBOW_FLAG = create("rainbow_flag");
public static final ResourceLocation TRANS_FLAG = create("trans_flag");
private static ResourceKey<TurtleOverlay> create(String name) {
return ResourceKey.create(TurtleOverlay.REGISTRY, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name));
private static ResourceLocation create(String name) {
return ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
}
private TurtleOverlays() {
}
public static void register(BootstrapContext<TurtleOverlay> registry) {
registry.register(RAINBOW_FLAG, new TurtleOverlay(
public static void register(BiConsumer<ResourceLocation, TurtleOverlay.Unbaked> registry) {
registry.accept(RAINBOW_FLAG, new TurtleOverlay.Unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_rainbow_overlay"),
true
));
registry.register(TRANS_FLAG, new TurtleOverlay(
registry.accept(TRANS_FLAG, new TurtleOverlay.Unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_trans_overlay"),
true
));

View File

@@ -6,6 +6,10 @@ package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.client.turtle.BasicUpgradeModel;
import dan200.computercraft.api.client.turtle.ItemUpgradeModel;
import dan200.computercraft.api.client.turtle.SelectUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable;
@@ -17,21 +21,21 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.function.BiConsumer;
import static dan200.computercraft.api.turtle.TurtleToolBuilder.tool;
class TurtleUpgradeProvider {
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(id("speaker"), new TurtleSpeaker(new ItemStack(ModRegistry.Items.SPEAKER.get())));
upgrades.register(vanilla("crafting_table"), new TurtleCraftingTable(new ItemStack(Items.CRAFTING_TABLE)));
upgrades.register(id("wireless_modem_normal"), new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), false));
upgrades.register(id("wireless_modem_advanced"), new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get()), true));
private static final ResourceKey<ITurtleUpgrade> SPEAKER = id("speaker");
private static final ResourceKey<ITurtleUpgrade> CRAFTING_TABLE = vanilla("crafting_table");
private static final ResourceKey<ITurtleUpgrade> WIRELESS_MODEM_NORMAL = id("wireless_modem_normal");
private static final ResourceKey<ITurtleUpgrade> WIRELESS_MODEM_ADVANCED = id("wireless_modem_advanced");
tool(vanilla("diamond_axe").location(), Items.DIAMOND_AXE).damageMultiplier(6.0f).register(upgrades);
tool(vanilla("diamond_pickaxe"), Items.DIAMOND_PICKAXE).register(upgrades);
tool(vanilla("diamond_hoe"), Items.DIAMOND_HOE).breakable(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).register(upgrades);
tool(vanilla("diamond_shovel"), Items.DIAMOND_SHOVEL).breakable(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).register(upgrades);
tool(vanilla("diamond_sword"), Items.DIAMOND_SWORD).breakable(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).register(upgrades);
}
private static final ResourceKey<ITurtleUpgrade> DIAMOND_AXE = vanilla("diamond_axe");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_PICKAXE = vanilla("diamond_pickaxe");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_HOE = vanilla("diamond_hoe");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_SHOVEL = vanilla("diamond_shovel");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_SWORD = vanilla("diamond_sword");
private static ResourceKey<ITurtleUpgrade> id(String id) {
return ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, id));
@@ -41,4 +45,52 @@ class TurtleUpgradeProvider {
// Naughty, please don't do this. Mostly here for some semblance of backwards compatibility.
return ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath("minecraft", id));
}
public static void register(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(SPEAKER, new TurtleSpeaker(new ItemStack(ModRegistry.Items.SPEAKER.get())));
upgrades.register(CRAFTING_TABLE, new TurtleCraftingTable(new ItemStack(Items.CRAFTING_TABLE)));
upgrades.register(WIRELESS_MODEM_NORMAL, new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), false));
upgrades.register(WIRELESS_MODEM_ADVANCED, new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get()), true));
tool(DIAMOND_AXE, Items.DIAMOND_AXE).damageMultiplier(6.0f).register(upgrades);
tool(DIAMOND_PICKAXE, Items.DIAMOND_PICKAXE).register(upgrades);
tool(DIAMOND_HOE, Items.DIAMOND_HOE).breakable(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).register(upgrades);
tool(DIAMOND_SHOVEL, Items.DIAMOND_SHOVEL).breakable(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).register(upgrades);
tool(DIAMOND_SWORD, Items.DIAMOND_SWORD).breakable(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).register(upgrades);
}
public static void addModels(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> out) {
out.accept(SPEAKER.location(), BasicUpgradeModel.unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
out.accept(CRAFTING_TABLE.location(), BasicUpgradeModel.unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
out.accept(WIRELESS_MODEM_NORMAL.location(), createModemModel("normal"));
out.accept(WIRELESS_MODEM_ADVANCED.location(), createModemModel("advanced"));
out.accept(DIAMOND_AXE.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_PICKAXE.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_HOE.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_SHOVEL.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_SWORD.location(), ItemUpgradeModel.unbaked());
}
private static TurtleUpgradeModel.Unbaked createModemModel(String type) {
return SelectUpgradeModel.onComponent(ModRegistry.DataComponents.ON.get())
.when(false, createBaseModemModel(type, "off"))
.when(true, createBaseModemModel(type, "on"))
.fallback(createBaseModemModel(type, "off"))
.create();
}
private static TurtleUpgradeModel.Unbaked createBaseModemModel(String type, String state) {
return BasicUpgradeModel.unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_" + state + "_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_" + state + "_right")
);
}
}

View File

@@ -4,12 +4,14 @@
package dan200.computercraft.data.client;
import com.mojang.math.Quadrant;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.item.model.TurtleOverlayModel;
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
@@ -20,12 +22,17 @@ import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock;
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.color.item.Dye;
import net.minecraft.client.data.models.BlockModelGenerators;
import net.minecraft.client.data.models.blockstates.*;
import net.minecraft.client.data.models.MultiVariant;
import net.minecraft.client.data.models.blockstates.ConditionBuilder;
import net.minecraft.client.data.models.blockstates.MultiPartGenerator;
import net.minecraft.client.data.models.blockstates.MultiVariantGenerator;
import net.minecraft.client.data.models.blockstates.PropertyDispatch;
import net.minecraft.client.data.models.model.*;
import net.minecraft.client.renderer.block.model.VariantMutator;
import net.minecraft.client.renderer.item.EmptyModel;
import net.minecraft.client.renderer.item.properties.conditional.HasComponent;
import net.minecraft.core.Direction;
@@ -33,7 +40,6 @@ import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.Property;
@@ -43,6 +49,7 @@ import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import static net.minecraft.client.data.models.BlockModelGenerators.*;
import static net.minecraft.client.data.models.model.ModelLocationUtils.getModelLocation;
import static net.minecraft.client.data.models.model.TextureMapping.getBlockTexture;
@@ -116,15 +123,14 @@ public class BlockModelProvider {
registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
generators.blockStateOutput.accept(
BlockModelGenerators.createSimpleBlock(ModRegistry.Blocks.LECTERN.get(), getModelLocation(Blocks.LECTERN))
.with(createHorizontalFacingDispatch())
createSimpleBlock(ModRegistry.Blocks.LECTERN.get(), plainVariant(getModelLocation(Blocks.LECTERN)))
.with(ROTATION_HORIZONTAL_FACING)
);
}
private static void registerDiskDrive(BlockModelGenerators generators) {
var diskDrive = ModRegistry.Blocks.DISK_DRIVE.get();
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(diskDrive)
.with(createHorizontalFacingDispatch())
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(diskDrive)
.with(createModelDispatch(DiskDriveBlock.STATE, value -> {
var textureSuffix = switch (value) {
case EMPTY -> "_front";
@@ -137,14 +143,14 @@ public class BlockModelProvider {
generators.modelOutput
);
}))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.registerSimpleItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
}
private static void registerPrinter(BlockModelGenerators generators) {
var printer = ModRegistry.Blocks.PRINTER.get();
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(printer)
.with(createHorizontalFacingDispatch())
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(printer)
.with(createModelDispatch(PrinterBlock.TOP, PrinterBlock.BOTTOM, (top, bottom) -> {
String model, texture;
if (top && bottom) {
@@ -165,13 +171,13 @@ public class BlockModelProvider {
generators.modelOutput
);
}))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.registerSimpleItemModel(printer, getModelLocation(printer, "_empty"));
}
private static void registerComputer(BlockModelGenerators generators, ComputerBlock<?> block) {
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createHorizontalFacingDispatch())
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
.with(createModelDispatch(ComputerBlock.STATE, state -> switch (state) {
case OFF -> ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
block, "_" + state.getSerializedName(),
@@ -184,6 +190,7 @@ public class BlockModelProvider {
generators.modelOutput
);
}))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.registerSimpleItemModel(block, getModelLocation(block, "_blinking"));
}
@@ -193,7 +200,7 @@ public class BlockModelProvider {
var particleModel = ModelTemplates.PARTICLE_ONLY.createWithSuffix(
block, "_particle", TextureMapping.particle(getBlockTexture(block, "_front")), generators.modelOutput
);
generators.blockStateOutput.accept(BlockModelGenerators.createSimpleBlock(block, particleModel));
generators.blockStateOutput.accept(createSimpleBlock(block, plainVariant(particleModel)));
// We then register the full model for use in items and the BE renderer.
var model = TURTLE.create(block, new TextureMapping()
@@ -210,7 +217,7 @@ public class BlockModelProvider {
generators.itemModelOutput.accept(block.asItem(), ItemModelUtils.composite(
ItemModelUtils.conditional(
new HasComponent(DataComponents.DYED_COLOR, false),
ItemModelUtils.plainModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL),
ItemModelUtils.tintedModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL, new Dye(-1)),
ItemModelUtils.plainModel(model)
),
new TurtleUpgradeModel.Unbaked(TurtleSide.LEFT, model),
@@ -224,17 +231,17 @@ public class BlockModelProvider {
}
private static void registerWirelessModem(BlockModelGenerators generators, WirelessModemBlock block) {
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createFacingDispatch())
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
.with(createModelDispatch(WirelessModemBlock.ON,
on -> modemModel(generators, getModelLocation(block, on ? "_on" : "_off"), getBlockTexture(block, "_face" + (on ? "_on" : "")))
)));
))
.with(ROTATION_FACING));
generators.registerSimpleItemModel(block, getModelLocation(block, "_off"));
}
private static void registerWiredModems(BlockModelGenerators generators) {
var fullBlock = ModRegistry.Blocks.WIRED_MODEM_FULL.get();
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(fullBlock)
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(fullBlock)
.with(createModelDispatch(WiredModemFullBlock.MODEM_ON, WiredModemFullBlock.PERIPHERAL_ON, (on, peripheral) -> {
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
var faceTexture = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_face" + (peripheral ? "_peripheral" : "") + (on ? "_on" : ""));
@@ -281,10 +288,10 @@ public class BlockModelProvider {
monitorModel(generators, block, "_u", 22, 5, 0, 38);
monitorModel(generators, block, "_ud", 21, 6, 0, 37);
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createHorizontalFacingDispatch())
.with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
.with(createModelDispatch(MonitorBlock.STATE, edge -> getModelLocation(block, edge == MonitorEdgeState.NONE ? "" : "_" + edge.getSerializedName())))
.with(ROTATION_HORIZONTAL_FACING)
.with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
);
generators.registerSimpleItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
}
@@ -308,55 +315,54 @@ public class BlockModelProvider {
var coreFacing = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_core_facing");
// Up/Down
generator.with(
Condition.or(
or(
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.UP, true),
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.DOWN, true)
),
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
plainVariant(coreFacing).with(VariantMutator.X_ROT.withValue(Quadrant.R90))
);
// North/South and no neighbours
generator.with(
Condition.or(
or(
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST),
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.NORTH, true),
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.SOUTH, true)
),
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R0)
plainVariant(coreFacing).with(VariantMutator.Y_ROT.withValue(Quadrant.R0))
);
// East/West
generator.with(
Condition.or(
or(
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.EAST, true),
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.WEST, true)
),
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R90)
plainVariant(coreFacing).with(VariantMutator.Y_ROT.withValue(Quadrant.R90))
);
// Find all other possibilities and emit a "solid" core which doesn't have a facing direction.
var core = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_core_any");
List<Condition.TerminalCondition> rightAngles = new ArrayList<>();
List<ConditionBuilder> rightAngles = new ArrayList<>();
for (var i = 0; i < DirectionUtil.FACINGS.length; i++) {
for (var j = i; j < DirectionUtil.FACINGS.length; j++) {
if (DirectionUtil.FACINGS[i].getAxis() == DirectionUtil.FACINGS[j].getAxis()) continue;
rightAngles.add(new Condition.TerminalCondition()
rightAngles.add(condition()
.term(CableBlock.CABLE, true).term(CABLE_DIRECTIONS[i], true).term(CABLE_DIRECTIONS[j], true)
);
}
}
generator.with(Condition.or(rightAngles.toArray(new Condition[0])), Variant.variant().with(VariantProperties.MODEL, core));
generator.with(or(rightAngles.toArray(new ConditionBuilder[0])), plainVariant(core));
// Then emit the actual cable arms
var arm = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_arm");
for (var direction : DirectionUtil.FACINGS) {
generator.with(
new Condition.TerminalCondition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
Variant.variant()
.with(VariantProperties.MODEL, arm)
.with(VariantProperties.X_ROT, toXAngle(direction.getOpposite()))
.with(VariantProperties.Y_ROT, toYAngle(direction.getOpposite()))
condition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
plainVariant(arm)
.with(VariantMutator.X_ROT.withValue(toXAngle(direction.getOpposite())))
.with(VariantMutator.Y_ROT.withValue(toYAngle(direction.getOpposite())))
);
}
@@ -366,11 +372,10 @@ public class BlockModelProvider {
for (var peripheral : BOOLEANS) {
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
generator.with(
new Condition.TerminalCondition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
Variant.variant()
.with(VariantProperties.MODEL, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
.with(VariantProperties.X_ROT, toXAngle(direction))
.with(VariantProperties.Y_ROT, toYAngle(direction))
condition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
plainVariant(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
.with(VariantMutator.X_ROT.withValue(toXAngle(direction)))
.with(VariantMutator.Y_ROT.withValue(toYAngle(direction)))
);
}
}
@@ -390,8 +395,8 @@ public class BlockModelProvider {
private static final BooleanProperty[] CABLE_DIRECTIONS = { CableBlock.DOWN, CableBlock.UP, CableBlock.NORTH, CableBlock.SOUTH, CableBlock.WEST, CableBlock.EAST };
private static final boolean[] BOOLEANS = new boolean[]{ false, true };
private static Condition.TerminalCondition cableNoNeighbour(Direction... directions) {
var condition = new Condition.TerminalCondition().term(CableBlock.CABLE, true);
private static ConditionBuilder cableNoNeighbour(Direction... directions) {
var condition = condition().term(CableBlock.CABLE, true);
for (var direction : directions) condition.term(CABLE_DIRECTIONS[direction.ordinal()], false);
return condition;
}
@@ -418,66 +423,47 @@ public class BlockModelProvider {
generators.registerSimpleItemModel(block, ModelLocationUtils.getModelLocation(block));
}
private static VariantProperties.Rotation toXAngle(Direction direction) {
private static Quadrant toXAngle(Direction direction) {
return switch (direction) {
default -> VariantProperties.Rotation.R0;
case UP -> VariantProperties.Rotation.R270;
case DOWN -> VariantProperties.Rotation.R90;
default -> Quadrant.R0;
case UP -> Quadrant.R270;
case DOWN -> Quadrant.R90;
};
}
private static VariantProperties.Rotation toYAngle(Direction direction) {
private static Quadrant toYAngle(Direction direction) {
return switch (direction) {
default -> VariantProperties.Rotation.R0;
case NORTH -> VariantProperties.Rotation.R0;
case SOUTH -> VariantProperties.Rotation.R180;
case EAST -> VariantProperties.Rotation.R90;
case WEST -> VariantProperties.Rotation.R270;
default -> Quadrant.R0;
case NORTH -> Quadrant.R0;
case SOUTH -> Quadrant.R180;
case EAST -> Quadrant.R90;
case WEST -> Quadrant.R270;
};
}
private static PropertyDispatch createHorizontalFacingDispatch() {
var dispatch = PropertyDispatch.property(BlockStateProperties.HORIZONTAL_FACING);
for (var direction : BlockStateProperties.HORIZONTAL_FACING.getPossibleValues()) {
dispatch.select(direction, Variant.variant().with(VariantProperties.Y_ROT, toYAngle(direction)));
}
return dispatch;
}
private static PropertyDispatch createVerticalFacingDispatch(Property<Direction> property) {
var dispatch = PropertyDispatch.property(property);
private static PropertyDispatch<VariantMutator> createVerticalFacingDispatch(Property<Direction> property) {
var dispatch = PropertyDispatch.modify(property);
for (var direction : property.getPossibleValues()) {
dispatch.select(direction, Variant.variant().with(VariantProperties.X_ROT, toXAngle(direction)));
dispatch.select(direction, VariantMutator.X_ROT.withValue(toXAngle(direction)));
}
return dispatch;
}
private static PropertyDispatch createFacingDispatch() {
var dispatch = PropertyDispatch.property(BlockStateProperties.FACING);
for (var direction : BlockStateProperties.FACING.getPossibleValues()) {
dispatch.select(direction, Variant.variant()
.with(VariantProperties.Y_ROT, toYAngle(direction))
.with(VariantProperties.X_ROT, toXAngle(direction))
);
}
return dispatch;
}
private static <T extends Comparable<T>> PropertyDispatch createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
var variant = PropertyDispatch.property(property);
private static <T extends Comparable<T>> PropertyDispatch<MultiVariant> createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
var variant = PropertyDispatch.initial(property);
for (var value : property.getPossibleValues()) {
variant.select(value, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(value)));
variant.select(value, plainVariant(makeModel.apply(value)));
}
return variant;
}
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch createModelDispatch(
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch<MultiVariant> createModelDispatch(
Property<T> propertyT, Property<U> propertyU, BiFunction<T, U, ResourceLocation> makeModel
) {
var variant = PropertyDispatch.properties(propertyT, propertyU);
var variant = PropertyDispatch.initial(propertyT, propertyU);
for (var valueT : propertyT.getPossibleValues()) {
for (var valueU : propertyU.getPossibleValues()) {
variant.select(valueT, valueU, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(valueT, valueU)));
variant.select(valueT, valueU, plainVariant(makeModel.apply(valueT, valueU)));
}
}
return variant;

View File

@@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data.client;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.client.model.ExtraModels;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
/**
* A data provider to generate {@link ExtraModels}.
*/
public abstract class ExtraModelsProvider implements DataProvider {
private final Path path;
private final CompletableFuture<HolderLookup.Provider> registries;
public ExtraModelsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
path = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(ExtraModels.PATH.getNamespace()).resolve(ExtraModels.PATH.getPath());
this.registries = registries;
}
/**
* Return a stream of models to load.
*
* @param registries The current registries.
* @return The collection of extra models to load.
*/
public abstract Stream<ResourceLocation> getModels(HolderLookup.Provider registries);
@Override
public final CompletableFuture<?> run(CachedOutput output) {
return registries.thenCompose(registries -> {
var models = new ExtraModels(getModels(registries).sorted().toList());
var json = ExtraModels.CODEC.encodeStart(JsonOps.INSTANCE, models).getOrThrow(IllegalStateException::new);
return DataProvider.saveStable(output, json, path);
});
}
@Override
public final String getName() {
return "Extra Models";
}
}

View File

@@ -11,7 +11,7 @@ import net.minecraft.world.item.ItemStack;
// @start region=body
public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade {
public ExampleTurtleUpgrade(ItemStack stack) {
super(TurtleUpgradeType.PERIPHERAL, "example", stack);
super(TurtleUpgradeType.PERIPHERAL, "upgrade.examplemod.example_turtle_upgrade.adjective", stack);
}
@Override

View File

@@ -2,6 +2,8 @@ package com.example.examplemod.data;
import com.example.examplemod.ExampleMod;
import com.example.examplemod.ExampleTurtleUpgrade;
import dan200.computercraft.api.client.turtle.ItemUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import net.minecraft.Util;
import net.minecraft.core.HolderLookup;
@@ -13,18 +15,19 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* Extends the bootstrap registries with our {@linkplain ExampleTurtleUpgrade example turtle upgrade}.
*/
// @start region=body
public class TurtleUpgradeProvider {
// Define our upgrade ids.
private static final ResourceLocation EXAMPLE_TURTLE_UPGRADE = ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade");
// Register our turtle upgrades.
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(
ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade")),
new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS))
);
upgrades.register(ITurtleUpgrade.createKey(EXAMPLE_TURTLE_UPGRADE), new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS)));
}
// Set up the dynamic registries to contain our turtle upgrades.
@@ -33,5 +36,10 @@ public class TurtleUpgradeProvider {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
}));
}
// Register our turtle models.
public static void addUpgradeModels(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> models) {
models.accept(EXAMPLE_TURTLE_UPGRADE, ItemUpgradeModel.unbaked());
}
}
// @end region=body

View File

@@ -28,7 +28,7 @@ public class BrewingStandPeripheral implements IPeripheral {
@LuaFunction
public final int getFuel() {
// Don't do it this way! Use an access widener/transformer to access the "fuel" field instead.
return brewingStand.saveWithoutMetadata(brewingStand.getLevel().registryAccess()).getInt("Fuel");
return brewingStand.saveWithoutMetadata(brewingStand.getLevel().registryAccess()).getByteOr("Fuel", (byte) 0);
}
@Override

View File

@@ -23,7 +23,7 @@ public class FurnacePeripheral implements GenericPeripheral {
@LuaFunction(mainThread = true)
public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
// Don't do it this way! Use an access widener/transformer to access the "litTime" field instead.
return furnace.saveWithoutMetadata(furnace.getLevel().registryAccess()).getInt("BurnTime");
return furnace.saveWithoutMetadata(furnace.getLevel().registryAccess()).getShortOr("lit_time_remaining", (short) 0);
}
}
// @end region=body

View File

@@ -1,14 +0,0 @@
{
"sources": [
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_advanced"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_command"},
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_command"},
{"type": "minecraft:single", "resource": "computercraft:gui/border_colour"},
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_colour"}
]
}

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