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

Compare commits

..

34 Commits

Author SHA1 Message Date
Jonathan Coates
4cccf1817c Update to Minecraft 1.21.8 2025-07-18 17:04:28 +01:00
Jonathan Coates
b8d9499027 Fix terminals not receiving scroll events
Minecraft 1.21.2 added a mouseScroll override to
AbstractContainerScreen, which means that child widgets no longer
receive scroll events. We reimplement that logic in our computer screen.

Fixes #2245.
2025-07-17 09:11:26 +01:00
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
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
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
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
0790a8346a Merge branch 'mc-1.21.x' into mc-1.21.y 2025-05-16 18:38:21 +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
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
a939ad8b97 Merge branch 'mc-1.21.x' into mc-1.21.y 2025-03-25 08:45:03 +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
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
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
9277aa33e9 Update to 1.21.4
Please don't talk to me about this. The first couple of hours of this
update were quite enjoyable, and then the rest was one of the most
miserable times I've had modding.

This has been a real slog, partly due to some large MC changes (item
models are a great change, but a pain to adapt to), and partly due to
mental health reasons — honestly, I've opened up my IDE so many times,
and then just closed it because I've hated the thought of even working
on this.

I will publish this to my maven, so mod authors can depend on it, but I
have no plans to publish a 1.21.4 version. 1.21.5 is right around the
corner (again, with some cool, but no-doubt painful changes), and I
need some time to focus on some breaking changes.

This commit actually includes the 1.21.3 update — the git history got so
messy here, so I just clobbered the whole thing. Sorry.

== Rendering ==

 - Remove TBO monitor renderer: There was a big overhaul to how shaders
   are defined and loaded in 1.21.2. It might have been possible to
   update the monitor shader code to this version, it doesn't see much
   use nowadays, so let's just delete it.

   This is a real shame — the TBO renderer was one of my favourite
   projects I've worked on. Unfortunately, it just doesn't seem worth
   the ongoing maintenance burden. It lives on in the standalone
   emulator :D.

 - Similarly, the VBO rendering code got a bit of an overhaul. We no
   longer use a custom VBO subclass, and instead just hack vanilla's to
   support changing the number of vertices rendered.

   This does mean we need to construct a MeshData, rather than a raw
   ByteBuffer. This isn't too hard, but not sure how it'll play with
   Iris. Given recent vanilla performance improvements, maybe we can
   remove our Unsafe code and use a normal BufferBuilder now.

 - Remove our custom emissive model code, now that vanilla supports
   it. We should add emissive textures to some other models at some
   point.

 - Remove mod-loader specific model code, and replace it with vanilla's
   ItemModel. This does constrain the design of turtle upgrade modellers
   quite a bit — we now only accept an untransformed BakedModel or a
   transformed ItemStack model. We may relax this in the future,
   unclear.

   This change does mean that updsidedown turtles are broken. RIP :(.

 - Entity rendering now separates reading state from the entity from
   actual rendering. This means we need to pass some extra state around
   for item frames. Easy on Forge, but requires a mixin on Fabric.

== Recipes ==

There were several major changes to ingredients this update. The code
here hasn't been very well tested right now — might be nice to add some
game tests for this.

 - Ingredients can no longer be constructed directly from a tag key (it
   needs to be fetched from the current registries), so the recipe
   generation code needs a bit of a reshuffle.

 - DiskRecipe now accepts a custom list of ingredients, rather than
   being hard-coded (fixes #1755). Recipes can now return custom
   `RecipeDisplay`s used to show a recipe in the crafting book. We use
   this to replace the impostor recipes.

   I'm not entirely sure how well this'll play with other recipe
   mods. Here's hoping.

 - Similarly, our recipe mod integration has been updated to use
   RecipeDisplay. We had to do this as ingredients no longer accept
   arbitrary ItemStacks (only a specific item).

== Misc ==

 - Blocks/items now need to know their ID ahead of time (so they can
   compute their description). This requires some reshuffling to the
   registration code, but it's pretty minor.

 - updateShape and neighborChanged no longer take a direction (the
   Orientation is mostly null) and so invalidates all redstone and
   peripherals.

 - All the positions were lowered by one in game tests. It's a good
   change (they now match the positions in structures), but annoying to
   update for!
2025-03-02 21:34:47 +00:00
517 changed files with 7721 additions and 8064 deletions

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

@@ -4,10 +4,7 @@
/** Default configuration for Fabric projects. */
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
import cc.tweaked.gradle.*
plugins {
`java-library`
@@ -67,3 +64,9 @@ dependencies {
tasks.ideaSyncTask {
doLast { IdeaRunConfigurations(project).patch() }
}
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.8")
}

View File

@@ -129,7 +129,7 @@ SPDX-License-Identifier: MPL-2.0
<module name="LocalFinalVariableName" />
<module name="LocalVariableName" />
<module name="MemberName">
<property name="format" value="^\$?[a-z][a-zA-Z0-9]*$" />
<property name="format" value="^(computercraft\$|\$)?[a-z][a-zA-Z0-9]*$" />
</module>
<module name="MethodName">
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />

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.116.0
modVersion=1.116.1
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.21.1
mcVersion=1.21.8

View File

@@ -7,23 +7,23 @@
# 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.102.1+1.21.1"
fabric-loader = "0.15.11"
neoForge = "21.1.9"
fabric-api = "0.129.0+1.21.8"
fabric-loader = "0.16.14"
neoForge = "21.8.0-beta"
neoMergeTool = "2.0.0"
mixin = "0.8.5"
parchment = "2024.07.28"
parchmentMc = "1.21"
yarn = "1.21.1+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.12"
guava = "32.1.2-jre"
netty = "4.1.97.Final"
slf4j = "2.0.9"
fastutil = "8.5.15"
guava = "33.3.1-jre"
netty = "4.1.118.Final"
slf4j = "2.0.16"
# Core dependencies (independent of Minecraft)
asm = "9.6"
asm = "9.7.1"
autoService = "1.1.1"
checkerFramework = "3.42.0"
cobalt = { strictly = "0.9.6" }
@@ -37,15 +37,15 @@ nightConfig = "3.8.1"
# Minecraft mods
emi = "1.1.7+1.21"
fabricPermissions = "0.3.1"
iris-fabric = "1.8.0-beta.3+1.21-fabric"
iris-forge = "1.8.0-beta.3+1.21-neoforge"
jei = "19.8.2.99"
modmenu = "11.0.0-rc.4"
fabricPermissions = "0.3.3"
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 = "16.0.729"
sodium-fabric = "mc1.21-0.6.0-beta.1-fabric"
sodium-forge = "mc1.21-0.6.0-beta.1-neoforge"
rei = "18.0.800"
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,7 +58,7 @@ junitPlatform = "1.11.4"
jmh = "1.37"
# Build tools
cctJavadoc = "1.8.4"
cctJavadoc = "1.8.5"
checkstyle = "10.23.1"
errorProne-core = "2.38.0"
errorProne-plugin = "4.1.0"
@@ -69,12 +69,12 @@ ideaExt = "1.1.7"
illuaminate = "0.1.0-83-g1131f68"
lwjgl = "3.3.3"
minotaur = "2.8.7"
modDevGradle = "2.0.95"
modDevGradle = "2.0.99"
nullAway = "0.12.7"
shadow = "8.3.1"
spotless = "7.0.2"
taskTree = "2.1.1"
teavm = "0.11.0-SQUID.1"
teavm = "0.13.0-SQUID.1"
vanillaExtract = "0.2.1"
versionCatalogUpdate = "0.8.1"
@@ -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" }
@@ -188,7 +188,7 @@ externalMods-common = ["iris-forge", "jei-api", "nightConfig-core", "nightConfig
externalMods-forge-compile = ["moreRed", "iris-forge", "jei-api"]
externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric-compile = ["fabricPermissions", "iris-fabric", "jei-api", "rei-api", "rei-builtin"]
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
externalMods-fabric-runtime = []
# Testing
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]

275
package-lock.json generated
View File

@@ -16,7 +16,7 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.0.0 && <12.1.3",
"@rollup/plugin-typescript": "^12.0.0",
"@rollup/plugin-url": "^8.0.1",
"@swc/core": "^1.3.92",
"@types/node": "^24.0.0",
@@ -481,9 +481,9 @@
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz",
"integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==",
"version": "12.1.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.4.tgz",
"integrity": "sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -554,9 +554,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
"integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz",
"integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==",
"cpu": [
"arm"
],
@@ -568,9 +568,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
"integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz",
"integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==",
"cpu": [
"arm64"
],
@@ -582,9 +582,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
"integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz",
"integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==",
"cpu": [
"arm64"
],
@@ -596,9 +596,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
"integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz",
"integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==",
"cpu": [
"x64"
],
@@ -610,9 +610,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
"integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz",
"integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==",
"cpu": [
"arm64"
],
@@ -624,9 +624,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
"integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz",
"integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==",
"cpu": [
"x64"
],
@@ -638,9 +638,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
"integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz",
"integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==",
"cpu": [
"arm"
],
@@ -652,9 +652,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
"integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz",
"integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==",
"cpu": [
"arm"
],
@@ -666,9 +666,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
"integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz",
"integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==",
"cpu": [
"arm64"
],
@@ -680,9 +680,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
"integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz",
"integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==",
"cpu": [
"arm64"
],
@@ -694,9 +694,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
"integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz",
"integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==",
"cpu": [
"loong64"
],
@@ -708,9 +708,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
"integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz",
"integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==",
"cpu": [
"ppc64"
],
@@ -722,9 +722,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
"integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz",
"integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==",
"cpu": [
"riscv64"
],
@@ -736,9 +736,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
"integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz",
"integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==",
"cpu": [
"riscv64"
],
@@ -750,9 +750,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
"integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz",
"integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==",
"cpu": [
"s390x"
],
@@ -764,9 +764,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
"integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz",
"integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==",
"cpu": [
"x64"
],
@@ -778,9 +778,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
"integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz",
"integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==",
"cpu": [
"x64"
],
@@ -792,9 +792,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
"integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz",
"integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==",
"cpu": [
"arm64"
],
@@ -806,9 +806,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
"integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz",
"integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==",
"cpu": [
"ia32"
],
@@ -820,9 +820,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
"integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz",
"integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==",
"cpu": [
"x64"
],
@@ -844,9 +844,9 @@
}
},
"node_modules/@swc/core": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.1.tgz",
"integrity": "sha512-aKXdDTqxTVFl/bKQZ3EQUjEMBEoF6JBv29moMZq0kbVO43na6u/u+3Vcbhbrh+A2N0X5OL4RaveuWfAjEgOmeA==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.9.tgz",
"integrity": "sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -862,16 +862,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.12.1",
"@swc/core-darwin-x64": "1.12.1",
"@swc/core-linux-arm-gnueabihf": "1.12.1",
"@swc/core-linux-arm64-gnu": "1.12.1",
"@swc/core-linux-arm64-musl": "1.12.1",
"@swc/core-linux-x64-gnu": "1.12.1",
"@swc/core-linux-x64-musl": "1.12.1",
"@swc/core-win32-arm64-msvc": "1.12.1",
"@swc/core-win32-ia32-msvc": "1.12.1",
"@swc/core-win32-x64-msvc": "1.12.1"
"@swc/core-darwin-arm64": "1.12.9",
"@swc/core-darwin-x64": "1.12.9",
"@swc/core-linux-arm-gnueabihf": "1.12.9",
"@swc/core-linux-arm64-gnu": "1.12.9",
"@swc/core-linux-arm64-musl": "1.12.9",
"@swc/core-linux-x64-gnu": "1.12.9",
"@swc/core-linux-x64-musl": "1.12.9",
"@swc/core-win32-arm64-msvc": "1.12.9",
"@swc/core-win32-ia32-msvc": "1.12.9",
"@swc/core-win32-x64-msvc": "1.12.9"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
@@ -883,9 +883,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.1.tgz",
"integrity": "sha512-nUjWVcJ3YS2N40ZbKwYO2RJ4+o2tWYRzNOcIQp05FqW0+aoUCVMdAUUzQinPDynfgwVshDAXCKemY8X7nN5MaA==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.9.tgz",
"integrity": "sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==",
"cpu": [
"arm64"
],
@@ -900,9 +900,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.1.tgz",
"integrity": "sha512-OGm4a4d3OeJn+tRt8H/eiHgTFrJbS6r8mi/Ob65tAEXZGHN900T2kR7c5ALr0V2hBOQ8BfhexwPoQlGQP/B95w==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.9.tgz",
"integrity": "sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==",
"cpu": [
"x64"
],
@@ -917,9 +917,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.1.tgz",
"integrity": "sha512-76YeeQKyK0EtNkQiNBZ0nbVGooPf9IucY0WqVXVpaU4wuG7ZyLEE2ZAIgXafIuzODGQoLfetue7I8boMxh1/MA==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.9.tgz",
"integrity": "sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==",
"cpu": [
"arm"
],
@@ -934,9 +934,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.1.tgz",
"integrity": "sha512-BxJDIJPq1+aCh9UsaSAN6wo3tuln8UhNXruOrzTI8/ElIig/3sAueDM6Eq7GvZSGGSA7ljhNATMJ0elD7lFatQ==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.9.tgz",
"integrity": "sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==",
"cpu": [
"arm64"
],
@@ -951,9 +951,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.1.tgz",
"integrity": "sha512-NhLdbffSXvY0/FwUSAl4hKBlpe5GHQGXK8DxTo3HHjLsD9sCPYieo3vG0NQoUYAy4ZUY1WeGjyxeq4qZddJzEQ==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.9.tgz",
"integrity": "sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==",
"cpu": [
"arm64"
],
@@ -968,9 +968,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.1.tgz",
"integrity": "sha512-CrYnV8SZIgArQ9LKH0xEF95PKXzX9WkRSc5j55arOSBeDCeDUQk1Bg/iKdnDiuj5HC1hZpvzwMzSBJjv+Z70jA==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.9.tgz",
"integrity": "sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==",
"cpu": [
"x64"
],
@@ -985,9 +985,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.1.tgz",
"integrity": "sha512-BQMl3d0HaGB0/h2xcKlGtjk/cGRn2tnbsaChAKcjFdCepblKBCz1pgO/mL7w5iXq3s57wMDUn++71/a5RAkZOA==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.9.tgz",
"integrity": "sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==",
"cpu": [
"x64"
],
@@ -1002,9 +1002,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.1.tgz",
"integrity": "sha512-b7NeGnpqTfmIGtUqXBl0KqoSmOnH64nRZoT5l4BAGdvwY7nxitWR94CqZuwyLPty/bLywmyDA9uO12Kvgb3+gg==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.9.tgz",
"integrity": "sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==",
"cpu": [
"arm64"
],
@@ -1019,9 +1019,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.1.tgz",
"integrity": "sha512-iU/29X2D7cHBp1to62cUg/5Xk8K+lyOJiKIGGW5rdzTW/c2zz3d/ehgpzVP/rqC4NVr88MXspqHU4il5gmDajw==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.9.tgz",
"integrity": "sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==",
"cpu": [
"ia32"
],
@@ -1036,9 +1036,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.1.tgz",
"integrity": "sha512-+Zh+JKDwiFqV5N9yAd2DhYVGPORGh9cfenu1ptr9yge+eHAf7vZJcC3rnj6QMR1QJh0Y5VC9+YBjRFjZVA7XDw==",
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.9.tgz",
"integrity": "sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==",
"cpu": [
"x64"
],
@@ -1124,9 +1124,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz",
"integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==",
"version": "24.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.8.tgz",
"integrity": "sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2856,13 +2856,13 @@
}
},
"node_modules/rollup": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
"integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
"version": "4.44.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz",
"integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -2872,36 +2872,29 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.43.0",
"@rollup/rollup-android-arm64": "4.43.0",
"@rollup/rollup-darwin-arm64": "4.43.0",
"@rollup/rollup-darwin-x64": "4.43.0",
"@rollup/rollup-freebsd-arm64": "4.43.0",
"@rollup/rollup-freebsd-x64": "4.43.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
"@rollup/rollup-linux-arm-musleabihf": "4.43.0",
"@rollup/rollup-linux-arm64-gnu": "4.43.0",
"@rollup/rollup-linux-arm64-musl": "4.43.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
"@rollup/rollup-linux-riscv64-gnu": "4.43.0",
"@rollup/rollup-linux-riscv64-musl": "4.43.0",
"@rollup/rollup-linux-s390x-gnu": "4.43.0",
"@rollup/rollup-linux-x64-gnu": "4.43.0",
"@rollup/rollup-linux-x64-musl": "4.43.0",
"@rollup/rollup-win32-arm64-msvc": "4.43.0",
"@rollup/rollup-win32-ia32-msvc": "4.43.0",
"@rollup/rollup-win32-x64-msvc": "4.43.0",
"@rollup/rollup-android-arm-eabi": "4.44.1",
"@rollup/rollup-android-arm64": "4.44.1",
"@rollup/rollup-darwin-arm64": "4.44.1",
"@rollup/rollup-darwin-x64": "4.44.1",
"@rollup/rollup-freebsd-arm64": "4.44.1",
"@rollup/rollup-freebsd-x64": "4.44.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.44.1",
"@rollup/rollup-linux-arm-musleabihf": "4.44.1",
"@rollup/rollup-linux-arm64-gnu": "4.44.1",
"@rollup/rollup-linux-arm64-musl": "4.44.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.44.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.44.1",
"@rollup/rollup-linux-riscv64-gnu": "4.44.1",
"@rollup/rollup-linux-riscv64-musl": "4.44.1",
"@rollup/rollup-linux-s390x-gnu": "4.44.1",
"@rollup/rollup-linux-x64-gnu": "4.44.1",
"@rollup/rollup-linux-x64-musl": "4.44.1",
"@rollup/rollup-win32-arm64-msvc": "4.44.1",
"@rollup/rollup-win32-ia32-msvc": "4.44.1",
"@rollup/rollup-win32-x64-msvc": "4.44.1",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup/node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",

View File

@@ -13,7 +13,7 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.0.0 && <12.1.3",
"@rollup/plugin-typescript": "^12.0.0",
"@rollup/plugin-url": "^8.0.1",
"@swc/core": "^1.3.92",
"@types/node": "^24.0.0",

View File

@@ -1,84 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import dan200.computercraft.impl.client.ClientPlatformHelper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import java.util.stream.Stream;
/**
* The location of a model to load. This may either be:
*
* <ul>
* <li>A {@link ModelResourceLocation}, referencing an already baked model (such as {@code minecraft:dirt#inventory}).</li>
* <li>
* A {@link ResourceLocation}, referencing a path to a model resource (such as {@code minecraft:item/dirt}.
* These models will be baked and stored in the {@link ModelManager} in a loader-specific way.
* </li>
* </ul>
*/
public final class ModelLocation {
/**
* The location of the model.
* <p>
* When {@link #resourceLocation} is null, this is the location of the model to load. When {@link #resourceLocation}
* is non-null, this is the "standalone" variant of the model resource this is used by NeoForge's implementation
* of {@link ClientPlatformHelper#getModel(ModelManager, ModelResourceLocation, ResourceLocation)} to fetch the
* model from the model manger. It is not used on Fabric.
*/
private final ModelResourceLocation modelLocation;
private final @Nullable ResourceLocation resourceLocation;
private ModelLocation(ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation) {
this.modelLocation = modelLocation;
this.resourceLocation = resourceLocation;
}
/**
* Create a {@link ModelLocation} from model in the model manager.
*
* @param location The name of the model to load.
* @return The new {@link ModelLocation} instance.
*/
public static ModelLocation ofModel(ModelResourceLocation location) {
return new ModelLocation(location, null);
}
/**
* Create a {@link ModelLocation} from a resource.
*
* @param location The location of the model resource, such as {@code minecraft:item/dirt}.
* @return The new {@link ModelLocation} instance.
*/
public static ModelLocation ofResource(ResourceLocation location) {
return new ModelLocation(new ModelResourceLocation(location, "standalone"), location);
}
/**
* Get this model from the provided model manager.
*
* @param manager The model manger.
* @return This model, or the missing model if it could not be found.
*/
public BakedModel getModel(ModelManager manager) {
return ClientPlatformHelper.get().getModel(manager, modelLocation, resourceLocation);
}
/**
* Get the models this model location depends on.
*
* @return A list of models that this model location depends on.
* @see TurtleUpgradeModeller#getDependencies()
*/
public Stream<ResourceLocation> getDependencies() {
return resourceLocation == null ? Stream.empty() : Stream.of(resourceLocation);
}
}

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,66 +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.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/**
* A model to render, combined with a transformation matrix to apply.
*
* @param model The model.
* @param matrix The transformation matrix.
*/
public record TransformedModel(BakedModel model, Transformation matrix) {
public TransformedModel(BakedModel model) {
this(model, Transformation.identity());
}
/**
* 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.
*/
public static TransformedModel of(ModelLocation location) {
var modelManager = Minecraft.getInstance().getModelManager();
return new TransformedModel(location.getModel(modelManager));
}
/**
* 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.
* @see ModelLocation#ofModel(ModelResourceLocation)
*/
public static TransformedModel of(ModelResourceLocation location) {
var modelManager = Minecraft.getInstance().getModelManager();
return new TransformedModel(modelManager.getModel(location));
}
/**
* 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.
* @see ModelLocation#ofResource(ResourceLocation)
*/
public static TransformedModel of(ResourceLocation location) {
var modelManager = Minecraft.getInstance().getModelManager();
return new TransformedModel(ClientPlatformHelper.get().getModel(modelManager, location));
}
public static TransformedModel of(ItemStack item, Transformation transform) {
var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(item);
return new TransformedModel(model, 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,112 +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.ModelLocation;
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#getDependencies()
*/
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 sided(ModelLocation.ofResource(left), ModelLocation.ofResource(right));
}
/**
* 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(ModelLocation left, ModelLocation 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).flatMap(ModelLocation::getDependencies);
}
};
}
}

View File

@@ -1,45 +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.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 dan200.computercraft.impl.client.ClientPlatformHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.core.component.DataComponentPatch;
import org.joml.Matrix4f;
import org.jspecify.annotations.Nullable;
final class TurtleUpgradeModellers {
private static final Transformation leftTransform = getMatrixFor(-0.4065f);
private static final Transformation rightTransform = getMatrixFor(0.4065f);
private static Transformation getMatrixFor(float offset) {
var matrix = new Matrix4f();
matrix.set(new float[]{
0.0f, 0.0f, -1.0f, 1.0f + offset,
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, -1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
});
matrix.transpose();
return new Transformation(matrix);
}
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) {
var stack = upgrade.getUpgradeItem(data);
var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(stack);
if (stack.hasFoil()) model = ClientPlatformHelper.get().createdFoiledModel(model);
return new TransformedModel(model, side == TurtleSide.LEFT ? leftTransform : rightTransform);
}
}
}

View File

@@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.client;
import dan200.computercraft.api.client.ModelLocation;
import dan200.computercraft.impl.Services;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.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.
* @see ModelLocation
*/
BakedModel getModel(ModelManager manager, ResourceLocation resourceLocation);
/**
* Set a model from a {@link ModelResourceLocation} or {@link ResourceLocation}.
* <p>
* This is largely equivalent to {@code resourceLocation == null ? manager.getModel(modelLocation) : getModel(manager, resourceLocation)},
* but allows pre-computing {@code modelLocation} (if needed).
*
* @param manager The model manager.
* @param modelLocation The location of the model to load.
* @param resourceLocation The location of the resource, if trying to load from a resource.
* @return The baked model.
* @see ModelLocation
*/
BakedModel getModel(ModelManager manager, ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation);
/**
* Wrap this model in a version which renders a foil/enchantment glint.
*
* @param model The model to wrap.
* @return The wrapped model.
* @see RenderType#glint()
*/
BakedModel createdFoiledModel(BakedModel model);
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

@@ -11,8 +11,6 @@ import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.IComputerSystem;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
@@ -142,19 +140,6 @@ public final class ComputerCraftAPI {
return getInstance().getBundledRedstoneOutput(world, pos, side);
}
/**
* Registers a media provider to provide {@link IMedia} implementations for Items.
*
* @param provider The media provider to register.
* @see MediaProvider
* @deprecated Prefer {@code dan200.computercraft.api.media.MediaLookup} (Fabric) or
* {@code dan200.computercraft.api.media.MediaCapability} (NeoForge).
*/
@Deprecated
public static void registerMediaProvider(MediaProvider provider) {
getInstance().registerMediaProvider(provider);
}
/**
* Attempt to get the game-wide wireless network.
*

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

@@ -1,26 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. This API may be redistributed unmodified and in full only.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.api.media;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* This interface is used to provide {@link IMedia} implementations for {@link ItemStack}.
*
* @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(MediaProvider)
*/
@FunctionalInterface
public interface MediaProvider {
/**
* Produce an IMedia implementation from an ItemStack.
*
* @param stack The stack from which to extract the media information.
* @return An {@link IMedia} implementation, or {@code null} if the item is not something you wish to handle
* @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(MediaProvider)
*/
@Nullable
IMedia getMedia(ItemStack stack);
}

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

@@ -4,17 +4,33 @@
package dan200.computercraft.api.turtle;
import com.mojang.serialization.Codec;
import net.minecraft.util.StringRepresentable;
/**
* An enum representing the two sides of the turtle that a turtle upgrade might reside.
*/
public enum TurtleSide {
public enum TurtleSide implements StringRepresentable {
/**
* The turtle's left side (where the pickaxe usually is on a Wireless Mining Turtle).
*/
LEFT,
LEFT("left"),
/**
* The turtle's right side (where the modem usually is on a Wireless Mining Turtle).
*/
RIGHT,
RIGHT("right");
public static final Codec<TurtleSide> CODEC = StringRepresentable.fromEnum(TurtleSide::values);
private final String name;
TurtleSide(String name) {
this.name = name;
}
@Override
public String getSerializedName() {
return name;
}
}

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

@@ -39,7 +39,7 @@ import java.util.function.Function;
* @see ITurtleUpgrade
* @see IPocketUpgrade
*/
public interface UpgradeType<T extends UpgradeBase> {
public sealed interface UpgradeType<T extends UpgradeBase> permits UpgradeTypeImpl {
/**
* The codec to read and write this upgrade from a datapack.
*

View File

@@ -12,7 +12,6 @@ import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.api.lua.ILuaAPIFactory;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.api.media.PrintoutContents;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.api.network.wired.WiredElement;
@@ -60,8 +59,6 @@ public interface ComputerCraftAPIService {
int getBundledRedstoneOutput(Level world, BlockPos pos, Direction side);
void registerMediaProvider(MediaProvider provider);
PacketNetwork getWirelessNetwork(MinecraftServer server);
void registerAPIFactory(ILuaAPIFactory factory);

View File

@@ -11,6 +11,12 @@ plugins {
id("cc-tweaked.publishing")
}
sourceSets.client {
java {
exclude("dan200/computercraft/client/integration/emi")
}
}
minecraft {
accessWideners(
"src/main/resources/computercraft.accesswidener",

View File

@@ -6,12 +6,13 @@ package dan200.computercraft.client;
import com.mojang.blaze3d.audio.Channel;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.CableHighlightRenderer;
import dan200.computercraft.client.render.ExtendedItemFrameRenderState;
import dan200.computercraft.client.render.PocketItemRenderer;
import dan200.computercraft.client.render.PrintoutItemRenderer;
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
import dan200.computercraft.client.render.monitor.MonitorHighlightRenderer;
import dan200.computercraft.client.render.monitor.MonitorRenderState;
import dan200.computercraft.client.sound.SpeakerManager;
@@ -29,16 +30,15 @@ import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
import net.minecraft.client.sounds.AudioStream;
import net.minecraft.client.sounds.SoundEngine;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.decoration.ItemFrame;
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;
@@ -91,9 +91,10 @@ public final class ClientHooks {
return false;
}
public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int light) {
if (stack.getItem() instanceof PrintoutItem) {
PrintoutItemRenderer.onRenderInFrame(transform, render, frame, stack, light);
public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, ExtendedItemFrameRenderState state, int light) {
if (state.printoutData != null) {
transform.mulPose(Axis.ZP.rotationDegrees(frame.rotation * 360.0f / 8.0f));
PrintoutItemRenderer.onRenderInFrame(transform, render, frame, state.printoutData, state.isBook, light);
return true;
}
@@ -135,31 +136,20 @@ public final class ClientHooks {
if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.holder().key().location()));
}
/**
* Add additional information about the game to the debug screen.
*
* @param addText A callback which adds a single line of text.
*/
public static void addGameDebugInfo(Consumer<String> addText) {
if (MonitorBlockEntityRenderer.hasRenderedThisFrame() && Minecraft.getInstance().getDebugOverlay().showDebugScreen()) {
addText.accept("[CC:T] Monitor renderer: " + MonitorBlockEntityRenderer.currentRenderer());
}
}
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,61 +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 dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.*;
import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.item.colour.PocketComputerLight;
import dan200.computercraft.client.item.model.TurtleOverlayModel;
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.RenderTypes;
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.core.util.Colour;
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.ComputerState;
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.ItemColor;
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.multiplayer.ClientLevel;
import net.minecraft.client.renderer.ShaderInstance;
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.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.network.chat.Component;
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.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.ResourceProvider;
import net.minecraft.util.FastColor;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.ItemLike;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
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.Supplier;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
@@ -69,11 +61,22 @@ import java.util.function.Supplier;
* @see ModRegistry The common registry for actual game objects.
*/
public final class ClientRegistry {
private static final Logger LOG = LoggerFactory.getLogger(ClientRegistry.class);
private ClientRegistry() {
}
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.
*/
@@ -85,25 +88,6 @@ public final class ClientRegistry {
BlockEntityRenderers.register(ModRegistry.BlockEntities.LECTERN.get(), CustomLecternRenderer::new);
}
/**
* Register any client-side objects which must be done on the main thread.
*
* @param itemProperties Callback to register item properties.
*/
public static void registerMainThread(RegisterItemProperty itemProperties) {
registerItemProperty(itemProperties, "state",
new UnclampedPropertyFunction((stack, world, player, random) -> {
var computer = ClientPocketComputers.get(stack);
return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
}),
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
);
registerItemProperty(itemProperties, "coloured",
(stack, world, player, random) -> DyedItemColor.getOrDefault(stack, -1) != -1 ? 1 : 0,
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
);
}
public static void registerMenuScreens(RegisterMenuScreen register) {
register.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
register.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
@@ -118,145 +102,102 @@ 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());
}
@SafeVarargs
private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
var id = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
for (var item : items) itemProperties.register(item.get(), id, getter);
}
/**
* Register an item property via {@link ItemProperties#register}. Forge and Fabric expose different methods, so we
* supply this via mod-loader-specific code.
*/
public interface RegisterItemProperty {
void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
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 = {
TurtleOverlay.ELF_MODEL,
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);
}
public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
register.accept(
(stack, layer) -> layer == 1 ? DyedItemColor.getOrDefault(stack, Colour.WHITE.getARGB()) : -1,
ModRegistry.Items.DISK.get()
);
register.accept(
(stack, layer) -> layer == 1 ? DyedItemColor.getOrDefault(stack, Colour.BLUE.getARGB()) : -1,
ModRegistry.Items.TREASURE_DISK.get()
);
register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get());
register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_NORMAL.get());
register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_ADVANCED.get());
}
private static int getPocketColour(ItemStack stack, int layer) {
return switch (layer) {
default -> -1;
case 1 -> DyedItemColor.getOrDefault(stack, -1); // Frame colour
case 2 -> { // Light colour
var computer = ClientPocketComputers.get(stack);
yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getARGB() : FastColor.ARGB32.opaque(computer.getLightState());
}
};
}
private static int getTurtleColour(ItemStack stack, int layer) {
return layer == 0 ? DyedItemColor.getOrDefault(stack, -1) : -1;
}
public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
RenderTypes.registerShaders(resources, (name, create, onLoaded) -> {
ShaderInstance shader;
try {
shader = create.get();
} catch (Exception e) {
LOG.error("Failed to load {}", name, e);
onLoaded.accept(null);
return;
}
load.accept(shader, onLoaded);
});
}
private record UnclampedPropertyFunction(
ClampedItemPropertyFunction function
) implements ClampedItemPropertyFunction {
@Override
public float unclampedCall(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
return function.unclampedCall(stack, level, entity, layer);
}
@Deprecated
@Override
public float call(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
return function.unclampedCall(stack, level, entity, layer);
}
/**
* 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
) {
}
/**
* Register client-side commands.
* Gather the list of extra models to load.
*
* @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.
* @param resources The current resource manager.
* @param executor The executor to schedule loading on.
* @return A promise which contains our extra models.
*/
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 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);
}
/**
* 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.
* A callback used to register a model for a {@link ModelKey}.
*/
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;
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);
}
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;
}
/**
* 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);
}
Util.getPlatform().openFile(file);
return 1;
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(dan200.computercraft.client.item.model.TurtleUpgradeModel.ID, dan200.computercraft.client.item.model.TurtleUpgradeModel.CODEC);
}
public static void registerItemColours(BiConsumer<ResourceLocation, MapCodec<? extends ItemTintSource>> register) {
register.accept(PocketComputerLight.ID, PocketComputerLight.CODEC);
}
public static void registerSelectItemProperties(BiConsumer<ResourceLocation, SelectItemModelProperty.Type<?, ?>> register) {
register.accept(PocketComputerStateProperty.ID, PocketComputerStateProperty.TYPE);
}
public static void registerConditionalItemProperties(BiConsumer<ResourceLocation, MapCodec<? extends ConditionalItemModelProperty>> register) {
register.accept(TurtleShowElfOverlay.ID, TurtleShowElfOverlay.CODEC);
}
public interface RegisterPictureInPictureRenderer {
<T extends PictureInPictureRenderState> void register(Class<T> state, Function<MultiBufferSource.BufferSource, PictureInPictureRenderer<T>> factory);
}
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

@@ -99,7 +99,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
new ItemToast(minecraft(), displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
.showOrReplace(minecraft().getToasts());
.showOrReplace(minecraft().getToastManager());
uploadNagDeadline = Long.MAX_VALUE;
}
}
@@ -125,6 +125,15 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
return super.mouseReleased(x, y, button);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
// Reimplement ContainerEventHandler.mouseScrolled, as AbstractContainerScreen overrides it.
var child = getChildAt(mouseX, mouseY);
if (child.isPresent() && child.get().mouseScrolled(mouseX, mouseY, scrollX, scrollY)) return true;
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
super.render(graphics, mouseX, mouseY, partialTicks);

View File

@@ -9,6 +9,7 @@ import dan200.computercraft.client.gui.widgets.TerminalWidget;
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;
@@ -41,11 +42,12 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra
var computerTextures = GuiSprites.getComputerTextures(family);
graphics.blitSprite(
computerTextures.border(),
RenderPipelines.GUI_TEXTURED, computerTextures.border(),
terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2
);
graphics.blitSprite(
Nullability.assertNonNull(computerTextures.sidebar()),
RenderPipelines.GUI_TEXTURED, Nullability.assertNonNull(computerTextures.sidebar()),
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
);
}

View File

@@ -7,6 +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.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@@ -23,7 +24,7 @@ public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
}
@Override

View File

@@ -5,9 +5,11 @@
package dan200.computercraft.client.gui;
import net.minecraft.client.Minecraft;
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.ToastComponent;
import net.minecraft.client.gui.components.toasts.ToastManager;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FormattedCharSequence;
@@ -35,8 +37,9 @@ public class ItemToast implements Toast {
private final Object token;
private final int width;
private boolean isNew = true;
private long firstDisplay;
private boolean changed = true;
private long lastChanged;
private Visibility visibility = Visibility.HIDE;
public ItemToast(Minecraft minecraft, ItemStack stack, Component title, Component message, Object token) {
this.stack = stack;
@@ -48,10 +51,10 @@ public class ItemToast implements Toast {
width = Math.max(MAX_LINE_SIZE, this.message.stream().mapToInt(font::width).max().orElse(MAX_LINE_SIZE)) + MARGIN * 3 + IMAGE_SIZE;
}
public void showOrReplace(ToastComponent toasts) {
public void showOrReplace(ToastManager toasts) {
var existing = toasts.getToast(ItemToast.class, getToken());
if (existing != null) {
existing.isNew = true;
existing.changed = true;
} else {
toasts.addToast(this);
}
@@ -73,28 +76,22 @@ public class ItemToast implements Toast {
}
@Override
public Visibility render(GuiGraphics graphics, ToastComponent component, long time) {
if (isNew) {
public Visibility getWantedVisibility() {
return visibility;
}
firstDisplay = time;
isNew = false;
@Override
public void update(ToastManager toastManager, long time) {
if (changed) {
lastChanged = time;
changed = false;
}
visibility = time - lastChanged < DISPLAY_TIME * toastManager.getNotificationDisplayTimeMultiplier() ? Visibility.SHOW : Visibility.HIDE;
}
if (width == 160 && message.size() <= 1) {
graphics.blitSprite(TEXTURE, 0, 0, width, height());
} else {
var height = height();
var bottom = Math.min(4, height - 28);
renderBackgroundRow(graphics, width, 0, 0, 28);
for (var i = 28; i < height - bottom; i += 10) {
renderBackgroundRow(graphics, width, 16, i, Math.min(16, height - i - bottom));
}
renderBackgroundRow(graphics, width, 32 - bottom, height - bottom, bottom);
}
@Override
public void render(GuiGraphics graphics, Font font, long time) {
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, width(), height());
var textX = MARGIN;
if (!stack.isEmpty()) {
@@ -102,23 +99,9 @@ public class ItemToast implements Toast {
graphics.renderFakeItem(stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE);
}
graphics.drawString(component.getMinecraft().font, title, textX, MARGIN, 0xff500050, false);
graphics.drawString(font, title, textX, MARGIN, 0xff500050, false);
for (var i = 0; i < message.size(); ++i) {
graphics.drawString(component.getMinecraft().font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
graphics.drawString(font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
}
return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
}
private static void renderBackgroundRow(GuiGraphics graphics, int x, int u, int y, int height) {
var leftOffset = u == 0 ? 20 : 5;
var rightOffset = Math.min(60, x - leftOffset);
graphics.blitSprite(TEXTURE, 160, 32, 0, u, 0, y, leftOffset, height);
for (var k = leftOffset; k < x - rightOffset; k += 64) {
graphics.blitSprite(TEXTURE, 160, 32, 32, u, k, y, Math.min(64, x - k - rightOffset), height);
}
graphics.blitSprite(TEXTURE, 160, 32, 160 - rightOffset, u, x - rightOffset, y, rightOffset, height);
}
}

View File

@@ -10,6 +10,7 @@ import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.ScrollWheelHandler;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess;
@@ -32,6 +33,8 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
private final Terminal terminalData;
private @Nullable TerminalWidget terminal;
private final ScrollWheelHandler scrollHandler = new ScrollWheelHandler();
public NoTermComputerScreen(T menu, Inventory player, Component title) {
super(title);
this.menu = menu;
@@ -67,7 +70,12 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
Objects.requireNonNull(minecraft().player).getInventory().swapPaint(scrollY);
var direction = scrollHandler.onMouseScroll(scrollX, scrollY);
var inventory = Objects.requireNonNull(minecraft().player).getInventory();
inventory.setSelectedSlot(ScrollWheelHandler.getNextScrollWheelSelection(
direction.y == 0 ? -direction.x : direction.y, inventory.getSelectedSlot(), Inventory.getSelectionSize()
));
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
}
@@ -100,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,6 +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.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
@@ -87,12 +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(BACKGROUND, x, y, 0, 0, innerWidth, PADDING);
graphics.blit(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
innerWidth, PADDING,
256, 256
);
graphics.blit(BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING);
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,6 +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.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@@ -23,9 +24,11 @@ public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
if (getMenu().isPrinting()) graphics.blit(BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45);
if (getMenu().isPrinting()) {
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
}
}
@Override

View File

@@ -4,28 +4,37 @@
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;
import static dan200.computercraft.client.render.PrintoutRenderer.*;
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
/**
* 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,13 +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);
drawBorder(graphics.pose(), graphics.bufferSource(), leftPos, topPos, 0, page, printout.pages(), printout.book(), FULL_BRIGHT_LIGHTMAP);
drawText(graphics.pose(), graphics.bufferSource(), leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, 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
@@ -144,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

@@ -12,6 +12,7 @@ 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.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
@@ -48,8 +49,11 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
var advanced = family == ComputerFamily.ADVANCED;
var texture = advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL;
graphics.blit(texture, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0, 0, TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE);
graphics.blit(
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
);
// Render selected slot
var slot = getMenu().getSelectedSlot();
@@ -57,14 +61,14 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
var slotX = slot % 4;
var slotY = slot / 4;
graphics.blitSprite(
advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
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
graphics.blitSprite(
Nullability.assertNonNull(GuiSprites.getComputerTextures(family).sidebar()),
RenderPipelines.GUI_TEXTURED, Nullability.assertNonNull(GuiSprites.getComputerTextures(family).sidebar()),
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
);
}

View File

@@ -4,12 +4,12 @@
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.systems.RenderSystem;
import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction;
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.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
@@ -48,10 +48,7 @@ public class DynamicImageButton extends Button {
setTooltip(message.tooltip());
var texture = this.texture.get(isHoveredOrFocused());
RenderSystem.disableDepthTest();
graphics.blitSprite(texture, getX(), getY(), 0, width, height);
RenderSystem.enableDepthTest();
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, texture, getX(), getY(), width, height);
}
public record HintedMessage(Component message, Tooltip tooltip) {

View File

@@ -4,8 +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.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
@@ -15,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;
@@ -255,12 +264,23 @@ public class TerminalWidget extends AbstractWidget {
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
if (!visible) return;
var emitter = FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), graphics.bufferSource().getBuffer(RenderTypes.TERMINAL));
var scissor = graphics.scissorStack.peek();
var terminalPose = new Matrix3x2f(graphics.pose());
var terminalTextures = TextureSetup.singleTextureWithLightmap(graphics.minecraft.getTextureManager().getTexture(FixedWidthFontRenderer.FONT).getTextureView());
FixedWidthFontRenderer.drawTerminal(
emitter,
(float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
);
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
@@ -275,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

@@ -6,12 +6,11 @@ package dan200.computercraft.client.integration;
import com.google.auto.service.AutoService;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.client.render.RenderTypes;
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.util.FastColor;
import net.minecraft.client.renderer.LightTexture;
import java.nio.ByteBuffer;
import java.util.Optional;
@@ -31,33 +30,31 @@ public class IrisShaderMod implements ShaderMod.Provider {
}
@Override
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> 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 IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, makeBuffer);
private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> builder) {
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, builder);
}
@Override
public ByteBuffer byteBuffer() {
return sink.getUnderlyingByteBuffer();
}
@Override
public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
sink.quad(x1, y1, x2, y2, z, colour, u1, v1, u2, v2, LightTexture.FULL_BRIGHT);
}
@Override
public VertexFormat format() {
return sink.getUnderlyingVertexFormat();
}
@Override
public ByteBuffer buffer() {
return sink.getUnderlyingByteBuffer();
}
@Override
public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
sink.quad(x1, y1, x2, y2, z, FastColor.ABGR32.fromArgb32(colour), u1, v1, u2, v2, RenderTypes.FULL_BRIGHT_LIGHTMAP);
}
}
}
}

View File

@@ -4,15 +4,15 @@
package dan200.computercraft.client.integration;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
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.
*/
@@ -31,16 +31,14 @@ public class ShaderMod {
}
/**
* Get an appropriate quad emitter for use with {@link DirectVertexBuffer} and {@link DirectFixedWidthFontRenderer} .
* Get an appropriate quad emitter for use with a vertex buffer and {@link DirectFixedWidthFontRenderer} .
*
* @param vertexCount The number of vertices.
* @param makeBuffer 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, IntFunction<ByteBuffer> makeBuffer) {
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(
makeBuffer.apply(RenderTypes.TERMINAL.format().getVertexSize() * vertexCount * 4)
);
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

@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.item.colour;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.pocket.PocketComputerData;
import net.minecraft.client.color.item.ItemTintSource;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ARGB;
import net.minecraft.util.ExtraCodecs;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* An {@link ItemTintSource} that returns the pocket computer's {@linkplain PocketComputerData#getLightState() light
* colour}.
*
* @param defaultColour The default colour, if the light is not currently on.
*/
public record PocketComputerLight(int defaultColour) implements ItemTintSource {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_light");
public static final MapCodec<PocketComputerLight> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ExtraCodecs.RGB_COLOR_CODEC.fieldOf("default").forGetter(PocketComputerLight::defaultColour)
).apply(instance, PocketComputerLight::new));
@Override
public int calculate(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder) {
var computer = ClientPocketComputers.get(stack);
return computer == null || computer.getLightState() == -1 ? defaultColour : ARGB.opaque(computer.getLightState());
}
@Override
public MapCodec<? extends ItemTintSource> type() {
return CODEC;
}
}

View File

@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
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.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.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.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* An {@link ItemModel} that renders the {@linkplain TurtleOverlay turtle overlay}.
*
* @param transforms The item transformations from the base model.
* @see TurtleOverlay#model()
*/
public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/overlay");
public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("transforms").forGetter(Unbaked::base)
).apply(instance, Unbaked::new));
@Override
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int light) {
var overlay = TurtleItem.getOverlay(stack);
if (overlay == null) return;
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 {
@Override
public MapCodec<Unbaked> type() {
return CODEC;
}
@Override
public ItemModel bake(BakingContext bakingContext) {
return new TurtleOverlayModel(bakingContext.blockModelBaker().getModel(base).getTopTransforms());
}
@Override
public void resolveDependencies(Resolver resolver) {
}
}
}

View File

@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
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.api.turtle.TurtleSide;
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.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.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* 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, 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),
ResourceLocation.CODEC.fieldOf("transforms").forGetter(Unbaked::base)
).apply(instance, Unbaked::new));
@Override
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;
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 {
@Override
public MapCodec<Unbaked> type() {
return CODEC;
}
@Override
public ItemModel bake(BakingContext bakingContext) {
return new TurtleUpgradeModel(side, bakingContext.blockModelBaker().getModel(base).getTopTransforms());
}
@Override
public void resolveDependencies(Resolver resolver) {
resolver.markDependency(base);
}
}
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
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;
import dan200.computercraft.shared.computer.core.ComputerState;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* A {@link SelectItemModelProperty} that returns the pocket computer's current state.
*/
public final class PocketComputerStateProperty implements SelectItemModelProperty<ComputerState> {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_state");
private static final PocketComputerStateProperty INSTANCE = new PocketComputerStateProperty();
public static final MapCodec<PocketComputerStateProperty> CODEC = MapCodec.unit(INSTANCE);
public static final Type<PocketComputerStateProperty, ComputerState> TYPE = Type.create(CODEC, ComputerState.CODEC);
private PocketComputerStateProperty() {
}
public static PocketComputerStateProperty create() {
return INSTANCE;
}
@Override
public ComputerState get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
var computer = ClientPocketComputers.get(stack);
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

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.item.properties;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
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;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* An item property that determines whether the turtle's current {@linkplain TurtleOverlay overlay} is compatible
* with the Christmas overlay.
*
* @see TurtleOverlay#showElfOverlay()
*/
public class TurtleShowElfOverlay implements ConditionalItemModelProperty {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/show_elf_overlay");
private static final TurtleShowElfOverlay INSTANCE = new TurtleShowElfOverlay();
public static final MapCodec<TurtleShowElfOverlay> CODEC = MapCodec.unit(INSTANCE);
@Override
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
return overlay == null || overlay.showElfOverlay();
}
public static TurtleShowElfOverlay create() {
return INSTANCE;
}
@Override
public MapCodec<? extends ConditionalItemModelProperty> type() {
return CODEC;
}
}

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

@@ -17,9 +17,9 @@ import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.item.component.DyedItemColor;
/**
@@ -34,11 +34,11 @@ public class LecternPocketModel {
public static final ResourceLocation TEXTURE_FRAME = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
public static final ResourceLocation TEXTURE_LIGHT = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_light");
private static final Material MATERIAL_NORMAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_NORMAL);
private static final Material MATERIAL_ADVANCED = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_ADVANCED);
private static final Material MATERIAL_COLOUR = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_COLOUR);
private static final Material MATERIAL_FRAME = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_FRAME);
private static final Material MATERIAL_LIGHT = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_LIGHT);
private static final Material MATERIAL_NORMAL = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_NORMAL);
private static final Material MATERIAL_ADVANCED = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_ADVANCED);
private static final Material MATERIAL_COLOUR = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_COLOUR);
private static final Material MATERIAL_FRAME = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_FRAME);
private static final Material MATERIAL_LIGHT = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_LIGHT);
// The size of the terminal within the model.
public static final float TERM_WIDTH = 12.0f / 32.0f;

View File

@@ -13,9 +13,9 @@ import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.InventoryMenu;
import java.util.List;
@@ -29,7 +29,7 @@ import java.util.List;
*/
public class LecternPrintoutModel {
public static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/printout");
public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE);
public static final Material MATERIAL = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE);
private static final int TEXTURE_WIDTH = 32;
private static final int TEXTURE_HEIGHT = 32;

View File

@@ -1,92 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model.turtle;
import com.mojang.math.Transformation;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.FaceBakery;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.Direction;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Applies a {@link Transformation} (or rather a {@link Matrix4f}) to a list of {@link BakedQuad}s.
* <p>
* This does a little bit of magic compared with other system (i.e. Forge's {@code QuadTransformers}), as it needs to
* handle flipping models upside down.
* <p>
* This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
*/
public class ModelTransformer {
private static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
private static final int STRIDE = FaceBakery.VERTEX_INT_SIZE;
private static final int POS_OFFSET = 0;
protected final Matrix4f transformation;
protected final boolean invert;
private @Nullable TransformedQuads cache;
public ModelTransformer(Transformation transformation) {
this.transformation = transformation.getMatrix();
invert = transformation.getMatrix().determinant() < 0;
}
public List<BakedQuad> transform(List<BakedQuad> quads) {
if (quads.isEmpty()) return List.of();
// We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
// so it's not worth being smarter here.
var cache = this.cache;
if (cache != null && quads.equals(cache.original())) return cache.transformed();
List<BakedQuad> transformed = new ArrayList<>(quads.size());
for (var quad : quads) transformed.add(transformQuad(quad));
this.cache = new TransformedQuads(quads, transformed);
return transformed;
}
private BakedQuad transformQuad(BakedQuad quad) {
var inputData = quad.getVertices();
var outputData = new int[inputData.length];
for (var i = 0; i < 4; i++) {
var inStart = STRIDE * i;
// Reverse the order of the quads if we're inverting
var outStart = getVertexOffset(i, invert);
System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
// Apply the matrix to our position
var inPosStart = inStart + POS_OFFSET;
var outPosStart = outStart + POS_OFFSET;
var x = Float.intBitsToFloat(inputData[inPosStart]);
var y = Float.intBitsToFloat(inputData[inPosStart + 1]);
var z = Float.intBitsToFloat(inputData[inPosStart + 2]);
// Transform the position
var pos = new Vector4f(x, y, z, 1);
transformation.transformProject(pos);
outputData[outPosStart] = Float.floatToRawIntBits(pos.x());
outputData[outPosStart + 1] = Float.floatToRawIntBits(pos.y());
outputData[outPosStart + 2] = Float.floatToRawIntBits(pos.z());
}
var direction = Direction.rotate(transformation, quad.getDirection());
return new BakedQuad(outputData, quad.getTintIndex(), direction, quad.getSprite(), quad.isShade());
}
public static int getVertexOffset(int vertex, boolean invert) {
return (invert ? ModelTransformer.INVERSE_ORDER[vertex] : vertex) * ModelTransformer.STRIDE;
}
private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
}
}

View File

@@ -1,146 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.client.model.turtle;
import com.google.common.cache.CacheBuilder;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Transformation;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.util.DataComponentUtil;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* Combines several individual models together to form a turtle.
*
* @param <T> The type of the resulting "baked model".
*/
public final class TurtleModelParts<T> {
private static final Transformation identity, flip;
static {
var stack = new PoseStack();
stack.translate(0.5f, 0.5f, 0.5f);
stack.scale(1, -1, 1);
stack.translate(-0.5f, -0.5f, -0.5f);
identity = Transformation.identity();
flip = new Transformation(stack.last().pose());
}
private record Combination(
boolean colour,
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade,
@Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
@Nullable TurtleOverlay overlay,
boolean christmas,
boolean flip
) {
}
private final BakedModel familyModel;
private final BakedModel colourModel;
private final Function<TransformedModel, BakedModel> transformer;
private final Function<Combination, T> buildModel;
/**
* A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed
* instances, reducing memory usage and hopefully ensuring their caches are hit more often!
*/
private final Map<TransformedModel, BakedModel> transformCache = CacheBuilder.newBuilder()
.concurrencyLevel(1)
.expireAfterAccess(30, TimeUnit.SECONDS)
.<TransformedModel, BakedModel>build()
.asMap();
/**
* A cache of {@link Combination}s to the combined model.
*/
private final Map<Combination, T> modelCache = CacheBuilder.newBuilder()
.concurrencyLevel(1)
.expireAfterAccess(30, TimeUnit.SECONDS)
.<Combination, T>build()
.asMap();
public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function<List<BakedModel>, T> combineModel) {
this.familyModel = familyModel;
this.colourModel = colourModel;
this.transformer = x -> transformer.transform(x.model(), x.matrix());
buildModel = x -> combineModel.apply(buildModel(x));
}
public T getModel(ItemStack stack) {
var combination = getCombination(stack);
return modelCache.computeIfAbsent(combination, buildModel);
}
private Combination getCombination(ItemStack stack) {
var christmas = Holiday.getCurrent() == Holiday.CHRISTMAS;
var leftUpgrade = TurtleItem.getUpgradeWithData(stack, TurtleSide.LEFT);
var rightUpgrade = TurtleItem.getUpgradeWithData(stack, TurtleSide.RIGHT);
var overlay = TurtleItem.getOverlay(stack);
var label = DataComponentUtil.getCustomName(stack);
var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm"));
return new Combination(stack.has(DataComponents.DYED_COLOR), leftUpgrade, rightUpgrade, overlay, christmas, flip);
}
private List<BakedModel> buildModel(Combination combo) {
var mc = Minecraft.getInstance();
var modelManager = mc.getItemRenderer().getItemModelShaper().getModelManager();
var transformation = combo.flip ? flip : identity;
var parts = new ArrayList<BakedModel>(4);
parts.add(transform(combo.colour() ? colourModel : familyModel, transformation));
if (combo.overlay() != null) addPart(parts, modelManager, transformation, combo.overlay().model());
var showChristmas = TurtleOverlay.showElfOverlay(combo.overlay(), combo.christmas());
if (showChristmas) addPart(parts, modelManager, transformation, TurtleOverlay.ELF_MODEL);
addUpgrade(parts, transformation, TurtleSide.LEFT, combo.leftUpgrade());
addUpgrade(parts, transformation, TurtleSide.RIGHT, combo.rightUpgrade());
return parts;
}
private void addPart(List<BakedModel> parts, ModelManager modelManager, Transformation transformation, ResourceLocation model) {
parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, model), transformation));
}
private void addUpgrade(List<BakedModel> parts, Transformation transformation, TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
if (upgrade == null) return;
var model = TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side);
parts.add(transform(model.model(), transformation.compose(model.matrix())));
}
private BakedModel transform(BakedModel model, Transformation transformation) {
if (transformation.equals(Transformation.identity())) return model;
return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer);
}
public interface ModelTransformer {
BakedModel transform(BakedModel model, Transformation transformation);
}
}

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;
@@ -22,11 +21,13 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.JukeboxSong;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.LevelEvent;
import org.jspecify.annotations.Nullable;
import java.util.UUID;
@@ -34,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) {
@@ -62,10 +62,14 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
@Override
public void handlePlayRecord(BlockPos pos, @Nullable Holder<JukeboxSong> song) {
var level = Minecraft.getInstance().level;
if (level == null) return;
if (song == null) {
Minecraft.getInstance().levelRenderer.stopJukeboxSongAndNotifyNearby(pos);
level.levelEvent(LevelEvent.SOUND_STOP_JUKEBOX_SONG, pos, 0);
} else {
Minecraft.getInstance().levelRenderer.playJukeboxSong(song, pos);
var id = level.registryAccess().lookupOrThrow(Registries.JUKEBOX_SONG).getIdOrThrow(song.value());
level.levelEvent(LevelEvent.SOUND_PLAY_JUKEBOX_SONG, pos, id);
}
}

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

@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2025 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.ClientHooks;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.util.ARGB;
import net.minecraft.util.CommonColors;
import net.minecraft.world.phys.BlockHitResult;
/**
* Utilities for rendering block outline.
*
* @see ClientHooks#drawHighlight(PoseStack, MultiBufferSource, Camera, BlockHitResult)
*/
public final class BlockOutlineRenderer {
private BlockOutlineRenderer() {
}
/**
* Render a block outline, handling both normal and high-contrast modes.
*
* @param bufferSource The buffer source.
* @param renderer The function to render a highlight.
* @see LevelRenderer#renderBlockOutline(Camera, MultiBufferSource.BufferSource, PoseStack, boolean)
*/
public static void render(MultiBufferSource bufferSource, Renderer renderer) {
var highContrast = Minecraft.getInstance().options.highContrastBlockOutline().get();
if (highContrast) renderer.render(bufferSource.getBuffer(RenderType.secondaryBlockOutline()), 0xff000000);
var colour = highContrast ? CommonColors.HIGH_CONTRAST_DIAMOND : ARGB.color(0x66, CommonColors.BLACK);
renderer.render(bufferSource.getBuffer(RenderType.lines()), colour);
}
@FunctionalInterface
public interface Renderer {
void render(VertexConsumer buffer, int colour);
}
}

View File

@@ -11,8 +11,7 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.client.Camera;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.util.Mth;
import net.minecraft.client.renderer.ShapeRenderer;
import net.minecraft.world.phys.BlockHitResult;
public final class CableHighlightRenderer {
@@ -27,11 +26,10 @@ public final class CableHighlightRenderer {
* @param camera The current camera.
* @param hit The block hit result for the current player.
* @return If we rendered a custom outline.
* @see net.minecraft.client.renderer.LevelRenderer#renderHitOutline
*/
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);
@@ -49,27 +47,9 @@ public final class CableHighlightRenderer {
var yOffset = pos.getY() - cameraPos.y();
var zOffset = pos.getZ() - cameraPos.z();
var buffer = bufferSource.getBuffer(RenderType.lines());
var matrix4f = transform.last().pose();
// TODO: Can we just accesstransformer out LevelRenderer.renderShape?
shape.forAllEdges((x1, y1, z1, x2, y2, z2) -> {
var xDelta = (float) (x2 - x1);
var yDelta = (float) (y2 - y1);
var zDelta = (float) (z2 - z1);
var len = Mth.sqrt(xDelta * xDelta + yDelta * yDelta + zDelta * zDelta);
xDelta = xDelta / len;
yDelta = yDelta / len;
zDelta = zDelta / len;
buffer
.addVertex(matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset))
.setColor(0, 0, 0, 0.4f)
.setNormal(transform.last(), xDelta, yDelta, zDelta);
buffer
.addVertex(matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset))
.setColor(0, 0, 0, 0.4f)
.setNormal(transform.last(), xDelta, yDelta, zDelta);
});
BlockOutlineRenderer.render(
bufferSource, (buffer, colour) -> ShapeRenderer.renderShape(transform, buffer, shape, xOffset, yOffset, zOffset, colour)
);
return true;
}

View File

@@ -19,11 +19,10 @@ import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.LecternRenderer;
import net.minecraft.util.FastColor;
import net.minecraft.util.ARGB;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.phys.Vec3;
@@ -40,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()));
@@ -72,7 +68,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
pocketModel.render(
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), DyedItemColor.getOrDefault(item, -1),
FastColor.ARGB32.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
ARGB.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
);
// Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
@@ -82,8 +78,8 @@ 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(RenderTypes.TERMINAL));
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT));
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

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.items.PrintoutData;
import dan200.computercraft.shared.media.items.PrintoutItem;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* Additional render state attached to a {@link ItemFrameRenderState}.
*
* @see dan200.computercraft.client.ClientHooks#onRenderItemFrame(PoseStack, MultiBufferSource, ItemFrameRenderState, ExtendedItemFrameRenderState, int)
*/
public class ExtendedItemFrameRenderState {
public @Nullable PrintoutData printoutData;
public boolean isBook;
/**
* Set up the render state from the {@link ItemFrame}'s {@link ItemStack}.
*
* @param stack The item frame's item.
*/
public void setup(ItemStack stack) {
if (stack.getItem() instanceof PrintoutItem) {
printoutData = PrintoutData.getOrEmpty(stack);
isBook = stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get();
} else {
printoutData = null;
isBook = false;
}
}
}

View File

@@ -1,98 +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.model.turtle.ModelTransformer;
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.world.item.ItemStack;
import org.joml.Vector4f;
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 pretty similar to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, ItemStack, int, int)},
* but supports inverted quads (i.e. those with a negative scale).
*
* @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();
var inverted = matrix.pose().determinant() < 0;
for (var bakedquad : quads) {
var tint = -1;
if (tints != null && bakedquad.isTinted()) {
var idx = bakedquad.getTintIndex();
if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
}
putBulkQuad(buffer, matrix, bakedquad, tint, lightmapCoord, overlayLight, inverted);
}
}
/**
* A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, float, int, int)} which
* will reverse vertex order when the matrix is inverted.
*
* @param buffer The buffer to draw to.
* @param pose The current matrix stack.
* @param quad The quad to draw.
* @param colour The tint for this quad.
* @param lightmapCoord The lightmap coordinate
* @param overlayLight The overlay light.
* @param invert Whether to reverse the order of this quad.
*/
private static void putBulkQuad(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, int colour, int lightmapCoord, int overlayLight, boolean invert) {
var matrix = pose.pose();
// It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
// Direction.rotate (so not out of nowhere!), but is a little suspicious.
var dirNormal = quad.getDirection().getNormal();
var vector = new Vector4f();
matrix.transform(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f, vector).normalize();
float normalX = vector.x(), normalY = vector.y(), normalZ = vector.z();
var vertices = quad.getVertices();
for (var vertex = 0; vertex < 4; vertex++) {
var i = ModelTransformer.getVertexOffset(vertex, invert);
var x = Float.intBitsToFloat(vertices[i]);
var y = Float.intBitsToFloat(vertices[i + 1]);
var z = Float.intBitsToFloat(vertices[i + 2]);
matrix.transform(x, y, z, 1, vector);
var u = Float.intBitsToFloat(vertices[i + 4]);
var v = Float.intBitsToFloat(vertices[i + 5]);
buffer.addVertex(
vector.x(), vector.y(), vector.z(),
colour, u, v, overlayLight, lightmapCoord,
normalX, normalY, normalZ
);
}
}
}

View File

@@ -14,9 +14,10 @@ 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.resources.metadata.gui.GuiSpriteScaling;
import net.minecraft.util.FastColor;
import net.minecraft.util.ARGB;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import org.joml.Matrix4f;
@@ -80,7 +81,7 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
renderLight(transform, bufferSource, lightColour, width, height);
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT));
if (terminal == null) {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
} else {
@@ -157,20 +158,20 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
}
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
var buffer = render.getBuffer(RenderTypes.TERMINAL);
var buffer = render.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
FixedWidthFontRenderer.drawQuad(
FixedWidthFontRenderer.toVertexConsumer(transform, buffer),
width - LIGHT_HEIGHT * 2, height + BORDER / 2.0f, 0.001f, LIGHT_HEIGHT * 2, LIGHT_HEIGHT,
FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP
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)
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)
36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false
);
private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) {

View File

@@ -9,8 +9,7 @@ import com.mojang.math.Axis;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.media.items.PrintoutData;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
import net.minecraft.world.item.ItemStack;
import static dan200.computercraft.client.render.PrintoutRenderer.*;
@@ -34,25 +33,22 @@ public final class PrintoutItemRenderer extends ItemMapLikeRenderer {
transform.scale(0.42f, 0.42f, -0.42f);
transform.translate(-0.5f, -0.48f, 0.0f);
drawPrintout(transform, render, stack, light);
drawPrintout(transform, render, PrintoutData.getOrEmpty(stack), stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get(), light);
}
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int packedLight) {
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, PrintoutData data, boolean isBook, int packedLight) {
// Move a little bit forward to ensure we're not clipping with the frame
transform.translate(0.0f, 0.0f, -0.001f);
transform.mulPose(Axis.ZP.rotationDegrees(180f));
transform.scale(0.95f, 0.95f, -0.95f);
transform.translate(-0.5f, -0.5f, 0.0f);
var light = frame.getType() == EntityType.GLOW_ITEM_FRAME ? 0xf000d2 : packedLight; // See getLightVal.
drawPrintout(transform, render, stack, light);
var light = frame.isGlowFrame ? 0xf000d2 : packedLight; // See getLightCoords.
drawPrintout(transform, render, data, isBook, light);
}
private static void drawPrintout(PoseStack transform, MultiBufferSource render, ItemStack stack, int light) {
var pageData = stack.getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
private static void drawPrintout(PoseStack transform, MultiBufferSource render, PrintoutData pageData, boolean book, int light) {
var pages = pageData.pages();
var book = stack.is(ModRegistry.Items.PRINTED_BOOK.get());
double width = LINE_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2;
double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2;

View File

@@ -11,6 +11,8 @@ import dan200.computercraft.core.terminal.Palette;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.shared.media.items.PrintoutData;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.resources.ResourceLocation;
import org.joml.Matrix4f;
import java.util.List;
@@ -23,6 +25,12 @@ import static dan200.computercraft.shared.media.items.PrintoutData.LINES_PER_PAG
* {@linkplain PrintoutItemRenderer in-hand/item frame printouts}.
*/
public final class PrintoutRenderer {
/**
* Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
* it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
*/
private static final RenderType BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
private static final float BG_SIZE = 256.0f;
/**
@@ -58,11 +66,14 @@ public final class PrintoutRenderer {
private static final int COVER_Y = Y_SIZE;
private static final int COVER_X = X_SIZE + 4 * X_FOLD_SIZE;
private static final float BOOK_Z_OFFSET = -0.1f;
private static final float PAGE_Z_OFFSET = -0.01f;
private PrintoutRenderer() {
}
public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, TextBuffer[] text, TextBuffer[] colours) {
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
var buffer = bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
for (var line = 0; line < LINES_PER_PAGE && line < text.length; line++) {
FixedWidthFontRenderer.drawString(emitter,
@@ -73,7 +84,7 @@ public final class PrintoutRenderer {
}
public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, List<PrintoutData.Line> lines) {
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
var buffer = bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
for (var line = 0; line < LINES_PER_PAGE && line < lines.size(); line++) {
var lineContents = lines.get(start + line);
@@ -90,7 +101,7 @@ public final class PrintoutRenderer {
var leftPages = page;
var rightPages = pages - page - 1;
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_BACKGROUND);
var buffer = bufferSource.getBuffer(BACKGROUND);
if (isBook) {
// Border
@@ -99,12 +110,12 @@ public final class PrintoutRenderer {
var right = x + X_SIZE + offset - 4;
// Left and right border
drawTexture(matrix, buffer, left - 4, y - 8, z - 0.02f, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
drawTexture(matrix, buffer, right, y - 8, z - 0.02f, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
drawTexture(matrix, buffer, left - 4, y - 8, z + BOOK_Z_OFFSET, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
drawTexture(matrix, buffer, right, y - 8, z + BOOK_Z_OFFSET, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
// Draw centre panel (just stretched texture, sorry).
drawTexture(matrix, buffer,
x - offset, y, z - 0.02f, X_SIZE + offset * 2, Y_SIZE,
x - offset, y, z + BOOK_Z_OFFSET, X_SIZE + offset * 2, Y_SIZE,
COVER_X + COVER_SIZE / 2.0f, COVER_SIZE, COVER_SIZE, Y_SIZE,
light
);
@@ -112,20 +123,20 @@ public final class PrintoutRenderer {
var borderX = left;
while (borderX < right) {
double thisWidth = Math.min(right - borderX, X_SIZE);
drawTexture(matrix, buffer, borderX, y - 8, z - 0.02f, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z - 0.02f, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
drawTexture(matrix, buffer, borderX, y - 8, z + BOOK_Z_OFFSET, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z + BOOK_Z_OFFSET, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
borderX = (float) (borderX + thisWidth);
}
}
// Current page background: Z-offset is interleaved between the "zeroth" left/right page and the first
// left/right page, so that the "bold" border can be drawn over the edge where appropriate.
drawTexture(matrix, buffer, x, y, z - 1e-3f * 0.5f, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
drawTexture(matrix, buffer, x, y, z + PAGE_Z_OFFSET, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
// Left pages
for (var n = 0; n <= leftPages; n++) {
drawTexture(matrix, buffer,
x - offsetAt(n), y, z - 1e-3f * n,
x - offsetAt(n), y, z + PAGE_Z_OFFSET * (n + 1),
// Use the left "bold" fold for the outermost page
n == leftPages ? 0 : X_FOLD_SIZE, 0,
X_FOLD_SIZE, Y_SIZE, light
@@ -135,7 +146,7 @@ public final class PrintoutRenderer {
// Right pages
for (var n = 0; n <= rightPages; n++) {
drawTexture(matrix, buffer,
x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z - 1e-3f * n,
x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z + PAGE_Z_OFFSET * (n + 1),
// Two folds, then the main page. Use the right "bold" fold for the outermost page.
X_FOLD_SIZE * 2 + X_SIZE + (n == rightPages ? X_FOLD_SIZE : 0), 0,
X_FOLD_SIZE, Y_SIZE, light

View File

@@ -1,99 +0,0 @@
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.RenderStateShard;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceProvider;
import org.apache.commons.io.function.IOSupplier;
import org.jspecify.annotations.Nullable;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Shared {@link RenderType}s used throughout the mod.
*/
public class RenderTypes {
public static final int FULL_BRIGHT_LIGHTMAP = (0xF << 4) | (0xF << 20);
private static @Nullable MonitorTextureBufferShader monitorTboShader;
/**
* Renders a fullbright terminal.
*/
public static final RenderType TERMINAL = RenderType.text(FixedWidthFontRenderer.FONT);
/**
* Renders a monitor with the TBO shader.
*
* @see MonitorTextureBufferShader
*/
public static final RenderType MONITOR_TBO = Types.MONITOR_TBO;
/**
* A variant of {@link #TERMINAL} which uses the lightmap rather than rendering fullbright.
*/
public static final RenderType PRINTOUT_TEXT = RenderType.text(FixedWidthFontRenderer.FONT);
/**
* Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
* it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
*/
public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
return monitorTboShader;
}
public static ShaderInstance getTerminalShader() {
return Objects.requireNonNull(GameRenderer.getRendertypeTextShader(), "Text shader has not been registered");
}
public interface ShaderLoader {
void tryLoad(String name, IOSupplier<ShaderInstance> create, Consumer<@Nullable ShaderInstance> accept) throws IOException;
}
public static void registerShaders(ResourceProvider resources, ShaderLoader load) throws IOException {
load.tryLoad("monitor shader", () -> new MonitorTextureBufferShader(
resources,
ComputerCraftAPI.MOD_ID + "/monitor_tbo",
MONITOR_TBO.format()
),
x -> monitorTboShader = (MonitorTextureBufferShader) x
);
}
private static final class Types extends RenderType {
private static final RenderStateShard.TextureStateShard TERM_FONT_TEXTURE = new TextureStateShard(
FixedWidthFontRenderer.FONT,
false, false // blur, minimap
);
static final RenderType MONITOR_TBO = RenderType.create(
"monitor_tbo", DefaultVertexFormat.POSITION_TEX, VertexFormat.Mode.TRIANGLE_STRIP, 128,
false, false, // useDelegate, needsSorting
RenderType.CompositeState.builder()
.setTextureState(TERM_FONT_TEXTURE)
.setShaderState(new ShaderStateShard(RenderTypes::getMonitorTextureBufferShader))
.createCompositeState(false)
);
@SuppressWarnings("UnusedMethod")
private Types(String name, VertexFormat format, VertexFormat.Mode mode, int buffer, boolean crumbling, boolean sort, Runnable setup, Runnable teardown) {
super(name, format, mode, buffer, crumbling, sort, setup, teardown);
}
}
}

View File

@@ -12,6 +12,7 @@ import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.resources.ResourceLocation;
import org.joml.Matrix4f;
/**
* 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.

View File

@@ -6,12 +6,13 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.mojang.math.Transformation;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.turtle.TurtleOverlay;
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.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
@@ -20,15 +21,17 @@ 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.FastColor;
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> {
public static final ResourceLocation NORMAL_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_normal");
public static final ResourceLocation ADVANCED_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_advanced");
public static final ResourceLocation COLOUR_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
private final BlockEntityRenderDispatcher renderer;
@@ -40,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.
@@ -72,31 +75,24 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.translate(0.5f, 0.5f, 0.5f);
var yaw = turtle.getRenderYaw(partialTicks);
transform.mulPose(Axis.YP.rotationDegrees(180.0f - yaw));
if (label != null && (label.equals("Dinnerbone") || label.equals("Grumm"))) {
transform.scale(1.0f, -1.0f, 1.0f);
}
transform.translate(-0.5f, -0.5f, -0.5f);
// Render the turtle
var colour = turtle.getColour();
var overlay = turtle.getOverlay();
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
if (colour == -1) {
// Render the turtle using its item model.
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper();
var model = modelManager.getItemModel(turtle.getBlockState().getBlock().asItem());
if (model == null) model = modelManager.getModelManager().getMissingModel();
renderModel(transform, buffers, lightmapCoord, overlayLight, model, null);
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
} else {
// Otherwise render it using the colour item.
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ FastColor.ARGB32.opaque(colour) });
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ ARGB.opaque(colour) });
}
// 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
@@ -107,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();
@@ -116,42 +112,15 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
transform.translate(0.0f, -0.5f, -0.5f);
var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
applyTransformation(transform, model.matrix());
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().getItemRenderer().getItemModelShaper().getModelManager();
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
var modelManager = Minecraft.getInstance().getModelManager();
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);
}
private static void applyTransformation(PoseStack stack, Transformation transformation) {
var trans = transformation.getTranslation();
stack.translate(trans.x(), trans.y(), trans.z());
stack.mulPose(transformation.getLeftRotation());
var scale = transformation.getScale();
stack.scale(scale.x(), scale.y(), scale.z());
stack.mulPose(transformation.getRightRotation());
}
}

View File

@@ -4,45 +4,41 @@
package dan200.computercraft.client.render.monitor;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.client.FrameInfo;
import dan200.computercraft.client.integration.ShaderMod;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.client.render.vbo.DirectBuffers;
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import dan200.computercraft.shared.util.DirectionUtil;
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.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL31;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.util.function.Consumer;
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> {
/**
@@ -53,13 +49,11 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
private static @Nullable ByteBuffer backingBuffer;
private static long lastFrame = -1;
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;
@@ -76,7 +70,6 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
return;
}
lastFrame = renderFrame;
renderState.lastRenderFrame = renderFrame;
renderState.lastRenderPos = monitorPos;
@@ -124,7 +117,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
transform.popPose();
} else {
FixedWidthFontRenderer.drawEmptyTerminal(
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
-MARGIN, MARGIN,
(float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2)
);
@@ -136,126 +129,137 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
private static void renderTerminal(
Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin
) {
int width = terminal.getWidth(), height = terminal.getHeight();
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
var renderType = currentRenderer();
var redraw = monitor.pollTerminalChanged();
if (renderState.createBuffer(renderType)) redraw = true;
if (renderState.vertexBuffer == null) redraw = true;
switch (renderType) {
case TBO -> {
if (redraw) {
var terminalBuffer = getBuffer(width * height * 3);
MonitorTextureBufferShader.setTerminalData(terminalBuffer, terminal);
DirectBuffers.setBufferData(GL31.GL_TEXTURE_BUFFER, renderState.tboBuffer, terminalBuffer, GL20.GL_STATIC_DRAW);
if (redraw) {
// 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);
var uniformBuffer = getBuffer(MonitorTextureBufferShader.UNIFORM_SIZE);
MonitorTextureBufferShader.setUniformData(uniformBuffer, terminal);
DirectBuffers.setBufferData(GL31.GL_UNIFORM_BUFFER, renderState.tboUniform, uniformBuffer, GL20.GL_STATIC_DRAW);
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin);
var vertexCountAfterBackground = sink.vertexCount();
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
var vertexCountAfterForeground = sink.vertexCount();
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
));
}
// Nobody knows what they're doing!
var active = GlStateManager._getActiveTexture();
RenderSystem.activeTexture(MonitorTextureBufferShader.TEXTURE_INDEX);
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, renderState.tboTexture);
RenderSystem.activeTexture(active);
var shader = RenderTypes.getMonitorTextureBufferShader();
shader.setupUniform(renderState.tboUniform);
var buffer = Tesselator.getInstance().begin(RenderTypes.MONITOR_TBO.mode(), RenderTypes.MONITOR_TBO.format());
tboVertex(buffer, matrix, -xMargin, -yMargin);
tboVertex(buffer, matrix, -xMargin, pixelHeight + yMargin);
tboVertex(buffer, matrix, pixelWidth + xMargin, -yMargin);
tboVertex(buffer, matrix, pixelWidth + xMargin, pixelHeight + yMargin);
RenderTypes.MONITOR_TBO.draw(Nullability.assertNonNull(buffer.build()));
}
case VBO -> {
var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
if (redraw) {
var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
// 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.
renderToBuffer(backgroundBuffer, size, sink ->
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
renderToBuffer(foregroundBuffer, size, 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);
});
// 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);
}
// 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.
// There's not really a good way around this, at least without using a custom render type (which the VBO
// renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
// absurdly high value.
var oldFogStart = RenderSystem.getShaderFogStart();
RenderSystem.setShaderFogStart(1e4f);
RenderTypes.TERMINAL.setupRenderState();
// Compose the existing model view matrix with our transformation matrix.
var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix);
// Render background geometry
backgroundBuffer.bind();
backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader());
// Render foreground geometry with glPolygonOffset enabled.
RenderSystem.polygonOffset(-1.0f, -10.0f);
RenderSystem.enablePolygonOffset();
foregroundBuffer.bind();
foregroundBuffer.drawWithShader(
modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader(),
// Skip the cursor quad if it is not visible this frame.
FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
? foregroundBuffer.getIndexCount() - RenderTypes.TERMINAL.mode().indexCount(4)
: foregroundBuffer.getIndexCount()
);
// Clear state
RenderSystem.polygonOffset(0.0f, -0.0f);
RenderSystem.disablePolygonOffset();
RenderTypes.TERMINAL.clearRenderState();
VertexBuffer.unbind();
RenderSystem.setShaderFogStart(oldFogStart);
}
case BEST -> throw new IllegalStateException("Impossible: Should never use BEST renderer");
}
}
private static void renderToBuffer(DirectVertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
var sink = ShaderMod.get().getQuadEmitter(size, MonitorBlockEntityRenderer::getBuffer);
var buffer = sink.buffer();
draw.accept(sink);
buffer.flip();
vbo.upload(buffer.limit() / sink.format().getVertexSize(), RenderTypes.TERMINAL.mode(), sink.format(), buffer);
}
private static void tboVertex(VertexConsumer builder, Matrix4f matrix, float x, float y) {
// We encode position in the UV, as that's not transformed by the matrix.
builder.addVertex(matrix, x, y, 0).setUv(x, y);
}
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);
renderState.vertexCountAfterBackground = vertexCountAfterBackground;
renderState.vertexCountAfterForeground = vertexCountAfterForeground;
renderState.vertexCountAfterCursor = vertexCountAfterCursor;
}
buffer.clear();
return buffer;
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.
// There's not really a good way around this, at least without using a custom render type (which the VBO
// 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(Minecraft.getInstance().gameRenderer.fogRenderer.getBuffer(FogRenderer.FogMode.NONE));
// Compose the existing model view matrix with our transformation matrix.
RenderSystem.getModelViewStack().pushMatrix();
RenderSystem.getModelViewStack().mul(matrix);
// Render background geometry
drawWithShader(renderState, FixedWidthFontRenderer.TERMINAL_TEXT, RenderPipelines.TEXT, 0, renderState.vertexCountAfterBackground);
drawWithShader(
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.getModelViewStack().popMatrix();
RenderSystem.setShaderFog(oldFog);
}
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 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);
}
renderType.clearRenderState();
}
@Override
@@ -268,27 +272,13 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
return monitor.getRenderBoundingBox();
}
/**
* Determine if any monitors were rendered this frame.
*
* @return Whether any monitors were rendered.
*/
public static boolean hasRenderedThisFrame() {
return FrameInfo.getRenderFrame() == lastFrame;
}
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);
}
/**
* Get the current renderer to use.
*
* @return The current renderer. Will not return {@link MonitorRenderer#BEST}.
*/
public static MonitorRenderer currentRenderer() {
var current = Config.monitorRenderer;
if (current == MonitorRenderer.BEST) current = Config.monitorRenderer = bestRenderer();
return current;
}
private static MonitorRenderer bestRenderer() {
return MonitorRenderer.VBO;
buffer.clear();
return buffer;
}
}

View File

@@ -6,13 +6,12 @@ package dan200.computercraft.client.render.monitor;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.render.BlockOutlineRenderer;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import net.minecraft.client.Camera;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.core.Direction;
import net.minecraft.world.phys.BlockHitResult;
import org.joml.Matrix4f;
import java.util.EnumSet;
@@ -30,11 +29,10 @@ 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();
var tile = world.getBlockEntity(pos);
if (!(tile instanceof MonitorBlockEntity monitor)) return false;
if (!(world.getBlockEntity(pos) instanceof MonitorBlockEntity monitor)) return false;
// Determine which sides are part of the external faces of the monitor, and so which need to be rendered.
var faces = EnumSet.allOf(Direction.class);
@@ -49,39 +47,37 @@ public final class MonitorHighlightRenderer {
transformStack.pushPose();
transformStack.translate(pos.getX() - cameraPos.x(), pos.getY() - cameraPos.y(), pos.getZ() - cameraPos.z());
// I wish I could think of a better way to do this
var buffer = bufferSource.getBuffer(RenderType.lines());
var transform = transformStack.last().pose();
var normal = transformStack.last();
if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 0, UP);
if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 1, UP);
if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 0, UP);
if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 1, UP);
if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, EAST);
if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 1, EAST);
if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, EAST);
if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 1, EAST);
if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, SOUTH);
if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, normal, 1, 0, 0, SOUTH);
if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, SOUTH);
if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, normal, 1, 1, 0, SOUTH);
var transform = transformStack.last();
BlockOutlineRenderer.render(bufferSource, (buffer, colour) -> draw(buffer, transform, faces, colour));
transformStack.popPose();
return true;
}
private static void line(VertexConsumer buffer, Matrix4f transform, PoseStack.Pose normal, float x, float y, float z, Direction direction) {
private static void draw(VertexConsumer buffer, PoseStack.Pose transform, EnumSet<Direction> faces, int colour) {
// I wish I could think of a better way to do this
if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, 0, 0, 0, UP, colour);
if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, 0, 0, 1, UP, colour);
if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, 1, 0, 0, UP, colour);
if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, 1, 0, 1, UP, colour);
if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 0, EAST, colour);
if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 1, EAST, colour);
if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, 0, 1, 0, EAST, colour);
if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, 0, 1, 1, EAST, colour);
if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 0, SOUTH, colour);
if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, 1, 0, 0, SOUTH, colour);
if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, 0, 1, 0, SOUTH, colour);
if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, 1, 1, 0, SOUTH, colour);
}
private static void line(VertexConsumer buffer, PoseStack.Pose transform, float x, float y, float z, Direction direction, int colour) {
buffer
.addVertex(transform, x, y, z)
.setColor(0, 0, 0, 0.4f)
.setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
.setColor(colour)
.setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
buffer
.addVertex(transform,
x + direction.getStepX(),
y + direction.getStepY(),
z + direction.getStepZ()
)
.setColor(0, 0, 0, 0.4f)
.setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
.addVertex(transform, x + direction.getStepX(), y + direction.getStepY(), z + direction.getStepZ())
.setColor(colour)
.setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
}
}

View File

@@ -5,17 +5,10 @@
package dan200.computercraft.client.render.monitor;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.mojang.blaze3d.platform.GlStateManager;
import dan200.computercraft.client.render.vbo.DirectBuffers;
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
import com.mojang.blaze3d.buffers.GpuBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraft.core.BlockPos;
import org.jspecify.annotations.Nullable;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.util.HashSet;
import java.util.Set;
@@ -27,97 +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 int tboBuffer;
public int tboTexture;
public int tboUniform;
public @Nullable DirectVertexBuffer backgroundBuffer;
public @Nullable DirectVertexBuffer foregroundBuffer;
@Nullable
GpuBuffer vertexBuffer;
/**
* Create the appropriate buffer if needed.
*
* @param renderer The renderer to use.
* @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(MonitorRenderer renderer) {
switch (renderer) {
case TBO: {
if (tboBuffer != 0) return false;
int vertexCountAfterBackground;
int vertexCountAfterForeground;
int vertexCountAfterCursor;
deleteBuffers();
void register() {
if (vertexBuffer != null) return;
tboBuffer = DirectBuffers.createBuffer();
DirectBuffers.setEmptyBufferData(GL31.GL_TEXTURE_BUFFER, tboBuffer, GL15.GL_STATIC_DRAW);
tboTexture = GlStateManager._genTexture();
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, tboTexture);
GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL30.GL_R8UI, tboBuffer);
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, 0);
tboUniform = DirectBuffers.createBuffer();
DirectBuffers.setEmptyBufferData(GL31.GL_UNIFORM_BUFFER, tboUniform, GL15.GL_STATIC_DRAW);
addMonitor();
return true;
}
case VBO:
if (backgroundBuffer != null) return false;
deleteBuffers();
backgroundBuffer = new DirectVertexBuffer();
foregroundBuffer = new DirectVertexBuffer();
addMonitor();
return true;
default:
return false;
}
}
private void addMonitor() {
synchronized (allMonitors) {
allMonitors.add(this);
}
}
private void deleteBuffers() {
if (tboBuffer != 0) {
DirectBuffers.deleteBuffer(GL31.GL_TEXTURE_BUFFER, tboBuffer);
tboBuffer = 0;
}
if (tboTexture != 0) {
GlStateManager._deleteTexture(tboTexture);
tboTexture = 0;
}
if (tboUniform != 0) {
DirectBuffers.deleteBuffer(GL31.GL_UNIFORM_BUFFER, tboUniform);
tboUniform = 0;
}
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 (tboBuffer != 0 || backgroundBuffer != null) {
if (vertexBuffer != null) {
synchronized (allMonitors) {
allMonitors.remove(this);
}

View File

@@ -1,127 +0,0 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render.monitor;
import com.mojang.blaze3d.shaders.Uniform;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.client.FrameInfo;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.server.packs.resources.ResourceProvider;
import org.jspecify.annotations.Nullable;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL31;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.getColour;
/**
* The shader used for the monitor TBO renderer.
* <p>
* This extends Minecraft's default shader loading code to extract out the TBO buffer and handle our custom uniforms
* ({@code MonitorData}, {@code CursorBlink}).
* <p>
* See also {@code monitor_tbo.fsh} and {@code monitor_tbo.vsh} in the mod's resources.
*
* @see RenderTypes#getMonitorTextureBufferShader()
*/
public class MonitorTextureBufferShader extends ShaderInstance {
private static final Logger LOG = LoggerFactory.getLogger(MonitorTextureBufferShader.class);
public static final int UNIFORM_SIZE = 4 * 4 * 16 + 4 + 4 + 2 * 4 + 4;
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
private final int monitorData;
private int uniformBuffer = 0;
private final @Nullable Uniform cursorBlink;
public MonitorTextureBufferShader(ResourceProvider provider, String location, VertexFormat format) throws IOException {
super(provider, location, format);
monitorData = GL31.glGetUniformBlockIndex(getId(), "MonitorData");
if (monitorData == -1) throw new IllegalStateException("Could not find MonitorData uniform.");
cursorBlink = getUniformChecked("CursorBlink");
var tbo = getUniformChecked("Tbo");
if (tbo != null) tbo.set(TEXTURE_INDEX - GL13.GL_TEXTURE0);
}
public void setupUniform(int buffer) {
uniformBuffer = buffer;
var cursorAlpha = FrameInfo.getGlobalCursorBlink() ? 1 : 0;
if (cursorBlink != null && cursorBlink.getIntBuffer().get(0) != cursorAlpha) cursorBlink.set(cursorAlpha);
}
@Override
public void apply() {
super.apply();
GL31.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, monitorData, uniformBuffer);
}
@Nullable
private Uniform getUniformChecked(String name) {
var uniform = getUniform(name);
if (uniform == null) {
LOG.warn("Monitor shader {} should have uniform {}, but it was not present.", getName(), name);
}
return uniform;
}
public static void setTerminalData(ByteBuffer buffer, Terminal terminal) {
int width = terminal.getWidth(), height = terminal.getHeight();
var pos = 0;
for (var y = 0; y < height; y++) {
TextBuffer text = terminal.getLine(y), textColour = terminal.getTextColourLine(y), background = terminal.getBackgroundColourLine(y);
for (var x = 0; x < width; x++) {
buffer.put(pos, (byte) (text.charAt(x) & 0xFF));
buffer.put(pos + 1, (byte) getColour(textColour.charAt(x), Colour.WHITE));
buffer.put(pos + 2, (byte) getColour(background.charAt(x), Colour.BLACK));
pos += 3;
}
}
buffer.limit(pos);
}
public static void setUniformData(ByteBuffer buffer, Terminal terminal) {
var pos = 0;
var palette = terminal.getPalette();
for (var i = 0; i < 16; i++) {
{
var colour = palette.getColour(i);
if (!terminal.isColour()) {
var f = FixedWidthFontRenderer.toGreyscale(colour);
buffer.putFloat(pos, f).putFloat(pos + 4, f).putFloat(pos + 8, f);
} else {
buffer.putFloat(pos, (float) colour[0]).putFloat(pos + 4, (float) colour[1]).putFloat(pos + 8, (float) colour[2]);
}
}
pos += 4 * 4; // std140 requires these are 4-wide
}
var showCursor = FixedWidthFontRenderer.isCursorVisible(terminal);
buffer
.putInt(pos, terminal.getWidth()).putInt(pos + 4, terminal.getHeight())
.putInt(pos + 8, showCursor ? terminal.getCursorX() : -2)
.putInt(pos + 12, showCursor ? terminal.getCursorY() : -2)
.putInt(pos + 16, 15 - terminal.getTextColour());
buffer.limit(UNIFORM_SIZE);
}
}

View File

@@ -7,12 +7,11 @@ package dan200.computercraft.client.render.text;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.core.terminal.Palette;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour;
import net.minecraft.util.FastColor;
import net.minecraft.util.ARGB;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
@@ -34,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}.
*/
@@ -157,26 +156,40 @@ 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;
ByteBuffer buffer();
public abstract ByteBuffer byteBuffer();
void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
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(ByteBuffer buffer) 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 RenderTypes.TERMINAL.format();
return TERMINAL_TEXT.format();
}
@Override
@@ -185,23 +198,24 @@ public final class DirectFixedWidthFontRenderer {
}
}
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) {
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)(rgba:BBBB)(uv1:FF)(uv2:SS),
// 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 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.
if (position < 0 || 112 > buffer.limit() - position) throw new IndexOutOfBoundsException();
// Require the pointer to be aligned to a 32-bit boundary.
if ((addr & 3) != 0) throw new IllegalStateException("Memory is not aligned");
if (TERMINAL_TEXT.format().getVertexSize() != 28) {
throw new IllegalStateException("Incorrect vertex size");
}
// Pack colour so it is equivalent to rgba:BBBB. This matches the logic in BufferBuilder.
var colourAbgr = FastColor.ABGR32.fromArgb32(colour);
var colourAbgr = ARGB.toABGR(colour);
// Pack colour so it is equivalent to abgr:BBBB. This matches the logic in BufferBuilder.
var nativeColour = IS_LITTLE_ENDIAN ? colourAbgr : Integer.reverseBytes(colourAbgr);
memPutFloat(addr + 0, x1);

View File

@@ -11,13 +11,13 @@ import dan200.computercraft.core.terminal.Palette;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FastColor;
import net.minecraft.util.ARGB;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
/**
* Handles rendering fixed width text and computer terminals.
* <p>
@@ -35,6 +35,13 @@ import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMA
public final class FixedWidthFontRenderer {
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;
@@ -42,7 +49,7 @@ public final class FixedWidthFontRenderer {
static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH;
static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
private static final int BLACK = ARGB.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
private static final float Z_OFFSET = 1e-4f;
private FixedWidthFontRenderer() {
@@ -137,7 +144,7 @@ public final class FixedWidthFontRenderer {
var rowY = y + FONT_HEIGHT * i;
drawString(
emitter, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
palette, FULL_BRIGHT_LIGHTMAP
palette, LightTexture.FULL_BRIGHT
);
}
}
@@ -152,12 +159,12 @@ public final class FixedWidthFontRenderer {
// Top and bottom margins
drawBackground(
emitter, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
leftMarginSize, rightMarginSize, topMarginSize, FULL_BRIGHT_LIGHTMAP
leftMarginSize, rightMarginSize, topMarginSize, LightTexture.FULL_BRIGHT
);
drawBackground(
emitter, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
leftMarginSize, rightMarginSize, bottomMarginSize, FULL_BRIGHT_LIGHTMAP
leftMarginSize, rightMarginSize, bottomMarginSize, LightTexture.FULL_BRIGHT
);
// The main text
@@ -165,7 +172,7 @@ public final class FixedWidthFontRenderer {
var rowY = y + FONT_HEIGHT * i;
drawBackground(
emitter, x, rowY, terminal.getBackgroundColourLine(i), palette,
leftMarginSize, rightMarginSize, FONT_HEIGHT, FULL_BRIGHT_LIGHTMAP
leftMarginSize, rightMarginSize, FONT_HEIGHT, LightTexture.FULL_BRIGHT
);
}
}
@@ -181,7 +188,7 @@ public final class FixedWidthFontRenderer {
public static void drawCursor(QuadEmitter emitter, float x, float y, Terminal terminal) {
if (isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()) {
var colour = terminal.getPalette().getRenderColours(15 - terminal.getTextColour());
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, FULL_BRIGHT_LIGHTMAP);
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, LightTexture.FULL_BRIGHT);
}
}
@@ -207,7 +214,7 @@ public final class FixedWidthFontRenderer {
}
public static void drawEmptyTerminal(QuadEmitter emitter, float x, float y, float width, float height) {
drawQuad(emitter, x, y, 0, width, height, BLACK, FULL_BRIGHT_LIGHTMAP);
drawQuad(emitter, x, y, 0, width, height, BLACK, LightTexture.FULL_BRIGHT);
}
public record QuadEmitter(Matrix4f poseMatrix, VertexConsumer consumer) {
@@ -220,11 +227,10 @@ public final class FixedWidthFontRenderer {
private static void quad(QuadEmitter c, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2, int light) {
var poseMatrix = c.poseMatrix();
var consumer = c.consumer();
int r = FastColor.ARGB32.red(colour), g = FastColor.ARGB32.green(colour), b = FastColor.ARGB32.blue(colour), a = FastColor.ARGB32.alpha(colour);
consumer.addVertex(poseMatrix, x1, y1, z).setColor(r, g, b, a).setUv(u1, v1).setLight(light);
consumer.addVertex(poseMatrix, x1, y2, z).setColor(r, g, b, a).setUv(u1, v2).setLight(light);
consumer.addVertex(poseMatrix, x2, y2, z).setColor(r, g, b, a).setUv(u2, v2).setLight(light);
consumer.addVertex(poseMatrix, x2, y1, z).setColor(r, g, b, a).setUv(u2, v1).setLight(light);
consumer.addVertex(poseMatrix, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
consumer.addVertex(poseMatrix, x1, y2, z).setColor(colour).setUv(u1, v2).setLight(light);
consumer.addVertex(poseMatrix, x2, y2, z).setColor(colour).setUv(u2, v2).setLight(light);
consumer.addVertex(poseMatrix, x2, y1, z).setColor(colour).setUv(u2, v1).setLight(light);
}
}

View File

@@ -1,71 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render.vbo;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferUploader;
import net.minecraft.Util;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL15C;
import org.lwjgl.opengl.GL45C;
import java.nio.ByteBuffer;
/**
* Provides utilities to interact with OpenGL's buffer objects, either using direct state access or binding/unbinding
* it.
*/
public class DirectBuffers {
public static final boolean HAS_DSA;
static final boolean ON_LINUX = Util.getPlatform() == Util.OS.LINUX;
static {
var capabilities = GL.getCapabilities();
HAS_DSA = capabilities.OpenGL45 || capabilities.GL_ARB_direct_state_access;
}
public static int createBuffer() {
return HAS_DSA ? GL45C.glCreateBuffers() : GL15C.glGenBuffers();
}
/**
* Delete a previously created buffer.
* <p>
* On Linux, {@link GlStateManager#_glDeleteBuffers(int)} clears a buffer before deleting it. However, this involves
* binding and unbinding the buffer, conflicting with {@link BufferUploader}'s cache. This deletion method uses
* our existing {@link #setEmptyBufferData(int, int, int)}, which correctly handles clearing the buffer.
*
* @param type The buffer's type.
* @param id The buffer's ID.
*/
public static void deleteBuffer(int type, int id) {
RenderSystem.assertOnRenderThread();
if (ON_LINUX) DirectBuffers.setEmptyBufferData(type, id, GL15C.GL_DYNAMIC_DRAW);
GL15C.glDeleteBuffers(id);
}
public static void setBufferData(int type, int id, ByteBuffer buffer, int flags) {
if (HAS_DSA) {
GL45C.glNamedBufferData(id, buffer, flags);
} else {
if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
GlStateManager._glBindBuffer(type, id);
GlStateManager._glBufferData(type, buffer, flags);
GlStateManager._glBindBuffer(type, 0);
}
}
public static void setEmptyBufferData(int type, int id, int flags) {
if (HAS_DSA) {
GL45C.glNamedBufferData(id, 0, flags);
} else {
if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
GlStateManager._glBindBuffer(type, id);
GlStateManager._glBufferData(type, 0, flags);
GlStateManager._glBindBuffer(type, 0);
}
}
}

View File

@@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.render.vbo;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferUploader;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.vertex.VertexFormat;
import net.minecraft.client.renderer.ShaderInstance;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL15C;
import org.lwjgl.opengl.GL45C;
import java.nio.ByteBuffer;
/**
* A version of {@link VertexBuffer} which allows uploading {@link ByteBuffer}s directly.
* <p>
* This should probably be its own class (rather than subclassing), but I need access to {@link VertexBuffer#drawWithShader}.
*/
public class DirectVertexBuffer extends VertexBuffer {
private int actualIndexCount;
public DirectVertexBuffer() {
super(Usage.STATIC);
if (DirectBuffers.HAS_DSA) {
RenderSystem.glDeleteBuffers(vertexBufferId);
if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
vertexBufferId = GL45C.glCreateBuffers();
}
}
public void upload(int vertexCount, VertexFormat.Mode mode, VertexFormat format, ByteBuffer buffer) {
bind();
this.mode = mode;
actualIndexCount = indexCount = mode.indexCount(vertexCount);
indexType = VertexFormat.IndexType.SHORT;
RenderSystem.assertOnRenderThread();
DirectBuffers.setBufferData(GL15.GL_ARRAY_BUFFER, vertexBufferId, buffer, GL15.GL_STATIC_DRAW);
if (format != this.format) {
if (this.format != null) this.format.clearBufferState();
this.format = format;
GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, vertexBufferId);
format.setupBufferState();
GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, 0);
}
var indexBuffer = RenderSystem.getSequentialBuffer(mode);
if (indexBuffer != sequentialIndices || !indexBuffer.hasStorage(indexCount)) {
indexBuffer.bind(indexCount);
sequentialIndices = indexBuffer;
}
}
public void drawWithShader(Matrix4f modelView, Matrix4f projection, ShaderInstance shader, int indexCount) {
this.indexCount = indexCount;
drawWithShader(modelView, projection, shader);
this.indexCount = actualIndexCount;
}
public int getIndexCount() {
return actualIndexCount;
}
@Override
public void close() {
super.close();
if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
}
}

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,61 +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.ModelLocation;
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(
ModelLocation leftOffModel, ModelLocation rightOffModel,
ModelLocation leftOnModel, ModelLocation rightOnModel
) {
private static final ModemModels NORMAL = create("normal");
private static final ModemModels ADVANCED = create("advanced");
public static ModemModels create(String type) {
return new ModemModels(
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left")),
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right")),
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left")),
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right"))
);
}
public Stream<ResourceLocation> getDependencies() {
return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel).flatMap(ModelLocation::getDependencies);
}
}
}

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,67 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import com.mojang.math.Transformation;
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) ->
new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
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,18 +5,23 @@
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.data.client.ExtraModelsProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.data.client.BlockModelProvider;
import dan200.computercraft.data.client.ItemModelProvider;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.Util;
import net.minecraft.client.data.models.BlockModelGenerators;
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;
@@ -24,7 +29,6 @@ import net.minecraft.data.PackOutput;
import net.minecraft.data.registries.RegistryPatchGenerator;
import net.minecraft.data.tags.TagsProvider;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
@@ -50,33 +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(out, fullRegistries));
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 ModelProvider(out, BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels));
generator.add(out -> new LanguageProvider(out, fullRegistries));
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
UpgradeSlot.LEFT_UPGRADE,
UpgradeSlot.RIGHT_UPGRADE,
generator.addFromCodec("Block atlases", PackOutput.Target.RESOURCE_PACK, "atlases", SpriteSources.FILE_CODEC, out -> {
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(
out.accept(AtlasIds.GUI, makeSprites(
Stream.of(UpgradeSlot.LEFT_UPGRADE, UpgradeSlot.RIGHT_UPGRADE),
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
@@ -87,12 +88,10 @@ public final class DataProviders {
generator.add(ResourceMetadataProvider::new);
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.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);
}
@SafeVarargs
@@ -106,11 +105,11 @@ public final class DataProviders {
<T extends DataProvider> T add(DataProvider.Factory<T> factory);
<T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
<T> void addFromCodec(String name, PackOutput.Target target, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
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.
@@ -118,5 +117,14 @@ public final class DataProviders {
* @param registries The patched registries to write.
*/
void registries(CompletableFuture<RegistrySetBuilder.PatchedRegistries> registries);
/**
* Generate block and item models.
*
* @param blocks The generator for block states and models.
* @param items The generator for item models.
* @see net.minecraft.client.data.models.ModelProvider
*/
void addModels(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items);
}
}

View File

@@ -1,97 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.data.models.ItemModelGenerators;
import net.minecraft.data.models.model.ModelTemplate;
import net.minecraft.data.models.model.ModelTemplates;
import net.minecraft.data.models.model.TextureMapping;
import net.minecraft.data.models.model.TextureSlot;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import java.util.Optional;
import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocation;
public final class ItemModelProvider {
private ItemModelProvider() {
}
public static void addItemModels(ItemModelGenerators generators) {
registerDisk(generators, ModRegistry.Items.DISK.get());
registerDisk(generators, ModRegistry.Items.TREASURE_DISK.get());
registerPocketComputer(generators, getModelLocation(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), false);
registerPocketComputer(generators, getModelLocation(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get()), false);
registerPocketComputer(generators, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_colour"), true);
generators.generateFlatItem(ModRegistry.Items.PRINTED_BOOK.get(), ModelTemplates.FLAT_ITEM);
generators.generateFlatItem(ModRegistry.Items.PRINTED_PAGE.get(), ModelTemplates.FLAT_ITEM);
generators.generateFlatItem(ModRegistry.Items.PRINTED_PAGES.get(), ModelTemplates.FLAT_ITEM);
}
private static void registerPocketComputer(ItemModelGenerators generators, ResourceLocation id, boolean off) {
createFlatItem(generators, id.withSuffix("_blinking"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_blink"),
id,
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_light")
);
createFlatItem(generators, id.withSuffix("_on"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_on"),
id,
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_light")
);
// Don't emit the default/off state for advanced/normal pocket computers, as they have item overrides.
if (off) {
createFlatItem(generators, id,
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_frame"),
id
);
}
}
private static void registerDisk(ItemModelGenerators generators, Item item) {
createFlatItem(generators, item,
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/disk_frame"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/disk_colour")
);
}
private static void createFlatItem(ItemModelGenerators generators, Item item, ResourceLocation... ids) {
createFlatItem(generators, getModelLocation(item), ids);
}
/**
* Generate a flat item from an arbitrary number of layers.
*
* @param generators The current item generator helper.
* @param model The model we're writing to.
* @param textures The textures which make up this model.
* @see net.minecraft.client.renderer.block.model.ItemModelGenerator The parser for this file format.
*/
private static void createFlatItem(ItemModelGenerators generators, ResourceLocation model, ResourceLocation... textures) {
if (textures.length > 5) throw new IndexOutOfBoundsException("Too many layers");
if (textures.length == 0) throw new IndexOutOfBoundsException("Must have at least one texture");
if (textures.length == 1) {
ModelTemplates.FLAT_ITEM.create(model, TextureMapping.layer0(textures[0]), generators.output);
return;
}
var slots = new TextureSlot[textures.length];
var mapping = new TextureMapping();
for (var i = 0; i < textures.length; i++) {
var slot = slots[i] = TextureSlot.create("layer" + i);
mapping.put(slot, textures[i]);
}
new ModelTemplate(Optional.of(ResourceLocation.withDefaultNamespace("item/generated")), Optional.empty(), slots)
.create(model, mapping, generators.output);
}
}

View File

@@ -100,8 +100,10 @@ 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");
@@ -279,19 +281,18 @@ public final class LanguageProvider implements DataProvider {
addConfigEntry(ConfigSpec.monitorWidth, "Max monitor width");
addConfigEntry(ConfigSpec.monitorHeight, "Max monitor height");
addConfigEntry(ConfigSpec.monitorRenderer, "Monitor renderer");
addConfigEntry(ConfigSpec.monitorDistance, "Monitor distance");
addConfigEntry(ConfigSpec.uploadNagDelay, "Upload nag delay");
}
private Stream<String> getExpectedKeys(HolderLookup.Provider registries) {
return Stream.of(
BuiltInRegistries.BLOCK.holders()
BuiltInRegistries.BLOCK.listElements()
.filter(x -> x.key().location().getNamespace().equals(ComputerCraftAPI.MOD_ID))
.map(x -> x.value().getDescriptionId())
// Exclude blocks that just reuse vanilla translations, such as the lectern.
.filter(x -> !x.startsWith("block.minecraft.")),
BuiltInRegistries.ITEM.holders()
BuiltInRegistries.ITEM.listElements()
.filter(x -> x.key().location().getNamespace().equals(ComputerCraftAPI.MOD_ID))
.map(x -> x.value().getDescriptionId()),
registries.lookupOrThrow(ITurtleUpgrade.REGISTRY).listElements().flatMap(x -> getTranslationKeys(x.value().getAdjective())),

View File

@@ -60,7 +60,7 @@ class LootTableProvider {
blockDrop(add, ModRegistry.Blocks.LECTERN, LootItem.lootTableItem(Items.LECTERN), ExplosionCondition.survivesExplosion());
add.accept(ModRegistry.Blocks.CABLE.get().getLootTable(), LootTable
add.accept(ModRegistry.Blocks.CABLE.get().getLootTable().orElseThrow(), LootTable
.lootTable()
.withPool(LootPool.lootPool()
.setRolls(ConstantValue.exactly(1))
@@ -115,7 +115,7 @@ class LootTableProvider {
LootItemCondition.Builder condition
) {
var block = wrapper.get();
add.accept(block.getLootTable(), LootTable
add.accept(block.getLootTable().orElseThrow(), LootTable
.lootTable()
.withPool(LootPool.lootPool()
.setRolls(ConstantValue.exactly(1))

View File

@@ -1,101 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data;
import com.google.gson.JsonElement;
import dan200.computercraft.shared.util.RegistryHelper;
import net.minecraft.Util;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.data.models.BlockModelGenerators;
import net.minecraft.data.models.ItemModelGenerators;
import net.minecraft.data.models.blockstates.BlockStateGenerator;
import net.minecraft.data.models.model.DelegatedModel;
import net.minecraft.data.models.model.ModelLocationUtils;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* A copy of {@link net.minecraft.data.models.ModelProvider} which accepts a custom generator.
* <p>
* Please don't sue me Mojang. Or at least make these changes to vanilla before doing so!
*/
public class ModelProvider implements DataProvider {
private final PackOutput.PathProvider blockStatePath;
private final PackOutput.PathProvider modelPath;
private final Consumer<BlockModelGenerators> blocks;
private final Consumer<ItemModelGenerators> items;
public ModelProvider(PackOutput output, Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) {
blockStatePath = output.createPathProvider(PackOutput.Target.RESOURCE_PACK, "blockstates");
modelPath = output.createPathProvider(PackOutput.Target.RESOURCE_PACK, "models");
this.blocks = blocks;
this.items = items;
}
@Override
public CompletableFuture<?> run(CachedOutput output) {
Map<Block, BlockStateGenerator> blockStates = new HashMap<>();
Consumer<BlockStateGenerator> addBlockState = generator -> {
var block = generator.getBlock();
if (blockStates.containsKey(block)) {
throw new IllegalStateException("Duplicate blockstate definition for " + block);
}
blockStates.put(block, generator);
};
Map<ResourceLocation, Supplier<JsonElement>> models = new HashMap<>();
BiConsumer<ResourceLocation, Supplier<JsonElement>> addModel = (id, contents) -> {
if (models.containsKey(id)) throw new IllegalStateException("Duplicate model definition for " + id);
models.put(id, contents);
};
Set<Item> explicitItems = new HashSet<>();
blocks.accept(new BlockModelGenerators(addBlockState, addModel, explicitItems::add));
items.accept(new ItemModelGenerators(addModel));
for (var block : BuiltInRegistries.BLOCK) {
if (!blockStates.containsKey(block)) continue;
var item = Item.BY_BLOCK.get(block);
if (item == null || explicitItems.contains(item)) continue;
var model = ModelLocationUtils.getModelLocation(item);
if (!models.containsKey(model)) {
models.put(model, new DelegatedModel(ModelLocationUtils.getModelLocation(block)));
}
}
List<CompletableFuture<?>> futures = new ArrayList<>();
saveCollection(output, futures, blockStates, x -> blockStatePath.json(RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK, x)));
saveCollection(output, futures, models, modelPath::json);
return Util.sequenceFailFast(futures);
}
private <T> void saveCollection(CachedOutput output, List<CompletableFuture<?>> futures, Map<T, ? extends Supplier<JsonElement>> items, Function<T, Path> getLocation) {
for (Map.Entry<T, ? extends Supplier<JsonElement>> entry : items.entrySet()) {
var path = getLocation.apply(entry.getKey());
futures.add(DataProvider.saveStable(output, entry.getValue().get(), path));
}
}
@Override
public String getName() {
return "Block State Definitions";
}
}

View File

@@ -4,14 +4,11 @@
package dan200.computercraft.data;
import com.google.gson.JsonObject;
import com.mojang.authlib.GameProfile;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.data.recipe.ShapedSpecBuilder;
import dan200.computercraft.data.recipe.ShapelessSpecBuilder;
import dan200.computercraft.shared.ModRegistry;
@@ -24,11 +21,9 @@ import dan200.computercraft.shared.platform.RecipeIngredients;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
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;
@@ -37,6 +32,7 @@ import dan200.computercraft.shared.util.RegistryHelper;
import net.minecraft.advancements.Criterion;
import net.minecraft.advancements.critereon.InventoryChangeTrigger;
import net.minecraft.advancements.critereon.ItemPredicate;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
@@ -44,15 +40,14 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.data.PackOutput;
import net.minecraft.data.recipes.RecipeCategory;
import net.minecraft.data.recipes.RecipeOutput;
import net.minecraft.data.recipes.ShapedRecipeBuilder;
import net.minecraft.data.recipes.ShapelessRecipeBuilder;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.ItemTags;
import net.minecraft.tags.TagKey;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.*;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.ResolvableProfile;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
@@ -62,68 +57,46 @@ import net.minecraft.world.level.ItemLike;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
import static dan200.computercraft.api.ComputerCraftTags.Items.WIRED_MODEM;
final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private final RecipeIngredients ingredients = PlatformHelper.get().getRecipeIngredients();
private final RecipeIngredients ingredients;
private final HolderGetter<Item> items;
private final CompletableFuture<HolderLookup.Provider> registries;
RecipeProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries);
this.registries = registries;
}
private HolderLookup.Provider registries() {
try {
return registries.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted");
} catch (ExecutionException e) {
var cause = e.getCause();
throw cause instanceof RuntimeException rt ? rt : new RuntimeException("Unexpected error", cause);
}
RecipeProvider(HolderLookup.Provider registries, RecipeOutput recipeOutput) {
super(registries, recipeOutput);
this.items = registries.lookupOrThrow(Registries.ITEM);
ingredients = PlatformHelper.get().getRecipeIngredients();
}
@Override
public void buildRecipes(RecipeOutput add) {
var registries = registries();
public void buildRecipes() {
basicRecipes();
diskColours();
pocketUpgrades();
turtleUpgrades();
turtleOverlays();
basicRecipes(add);
diskColours(add);
pocketUpgrades(add, registries);
turtleUpgrades(add, registries);
turtleOverlays(add, registries);
addSpecial(add, new DiskRecipe(CraftingBookCategory.MISC));
addSpecial(add, new ColourableRecipe(CraftingBookCategory.MISC));
addSpecial(add, new ClearColourRecipe(CraftingBookCategory.MISC));
addSpecial(add, new TurtleUpgradeRecipe(CraftingBookCategory.MISC));
addSpecial(add, new PocketComputerUpgradeRecipe(CraftingBookCategory.MISC));
special(new ColourableRecipe(CraftingBookCategory.MISC));
special(new ClearColourRecipe(CraftingBookCategory.MISC));
special(new TurtleUpgradeRecipe(CraftingBookCategory.MISC));
special(new PocketComputerUpgradeRecipe(CraftingBookCategory.MISC));
}
/**
* Register a crafting recipe for a disk of every dye colour.
*
* @param output The callback to add recipes.
* Register a disk recipe.
*/
private void diskColours(RecipeOutput output) {
for (var colour : Colour.VALUES) {
ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(ModRegistry.Items.DISK.get(), DataComponents.DYED_COLOR, new DyedItemColor(colour.getHex(), false)))
.requires(ingredients.redstone())
.requires(Items.PAPER)
.requires(DyeItem.byColor(ofColour(colour)))
.group("computercraft:disk")
.unlockedBy("has_drive", inventoryChange(ModRegistry.Items.DISK_DRIVE.get()))
.build(ImpostorShapelessRecipe::new)
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "disk_" + (colour.ordinal() + 1)));
}
private void diskColours() {
customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.DISK.get())
.requires(ingredients.redstone())
.requires(Items.PAPER)
.group("computercraft:disk")
.unlockedBy("has_drive", has(ModRegistry.Items.DISK_DRIVE.get()))
.build(d -> new DiskRecipe(d.properties(), d.ingredients()))
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "disk"));
}
private static List<TurtleItem> turtleItems() {
@@ -132,26 +105,22 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
/**
* Register a crafting recipe for each turtle upgrade.
*
* @param add The callback to add recipes.
* @param registries The currently available registries.
*/
private void turtleUpgrades(RecipeOutput add, HolderLookup.Provider registries) {
private void turtleUpgrades() {
for (var turtleItem : turtleItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
registries.lookupOrThrow(ITurtleUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
var upgrade = upgradeHolder.value();
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
.group(name.toString())
.pattern("#T")
.define('T', turtleItem)
.define('#', upgrade.getCraftingItem().getItem())
.unlockedBy("has_items", inventoryChange(turtleItem, upgrade.getCraftingItem().getItem()))
.unlockedBy("has_items", has(turtleItem, upgrade.getCraftingItem().getItem()))
.build(ImpostorShapedRecipe::new)
.save(
add,
output,
name.withSuffix(String.format("/%s/%s", upgradeHolder.key().location().getNamespace(), upgradeHolder.key().location().getPath()))
);
});
@@ -164,44 +133,40 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
/**
* Register a crafting recipe for each pocket upgrade.
*
* @param add The callback to add recipes.
* @param registries The currently available registries.
*/
private void pocketUpgrades(RecipeOutput add, HolderLookup.Provider registries) {
private void pocketUpgrades() {
for (var pocket : pocketComputerItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, pocket).withPath(x -> x.replace("pocket_computer_", "pocket_"));
registries.lookupOrThrow(IPocketUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
var upgrade = upgradeHolder.value();
ShapedSpecBuilder
.shaped(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")
.define('P', pocket)
.define('#', upgrade.getCraftingItem().getItem())
.unlockedBy("has_items", inventoryChange(pocket, upgrade.getCraftingItem().getItem()))
.unlockedBy("has_items", has(pocket, upgrade.getCraftingItem().getItem()))
.build(ImpostorShapedRecipe::new)
.save(
add,
output,
name.withSuffix(String.format("/%s/%s", upgradeHolder.key().location().getNamespace(), upgradeHolder.key().location().getPath()))
);
});
}
}
private void turtleOverlays(RecipeOutput add, HolderLookup.Provider registries) {
turtleOverlay(add, registries, TurtleOverlays.TRANS_FLAG, x -> x
.unlockedBy("has_dye", inventoryChange(itemPredicate(ingredients.dye())))
private void turtleOverlays() {
turtleOverlay(TurtleOverlays.TRANS_FLAG, x -> x
.unlockedBy("has_dye", has(ingredients.dye()))
.requires(ColourUtils.getDyeTag(DyeColor.LIGHT_BLUE))
.requires(ColourUtils.getDyeTag(DyeColor.PINK))
.requires(ColourUtils.getDyeTag(DyeColor.WHITE))
.requires(Items.STICK)
);
turtleOverlay(add, registries, TurtleOverlays.RAINBOW_FLAG, x -> x
.unlockedBy("has_dye", inventoryChange(itemPredicate(ingredients.dye())))
turtleOverlay(TurtleOverlays.RAINBOW_FLAG, x -> x
.unlockedBy("has_dye", has(ingredients.dye()))
.requires(ColourUtils.getDyeTag(DyeColor.RED))
.requires(ColourUtils.getDyeTag(DyeColor.ORANGE))
.requires(ColourUtils.getDyeTag(DyeColor.YELLOW))
@@ -212,264 +177,232 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
);
}
private void turtleOverlay(RecipeOutput add, HolderLookup.Provider registries, 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 = ShapelessSpecBuilder
.shapeless(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", inventoryChange(turtleItem));
.unlockedBy("has_turtle", has(turtleItem));
build.accept(builder);
builder
.requires(turtleItem)
.build(s -> new TransformShapelessRecipe(s, List.of(
CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
)))
.save(add, name.withSuffix("_overlays/" + overlay.location().getPath()));
.save(output, name.withSuffix("_overlays/" + overlay.getPath()));
}
}
private void basicRecipes(RecipeOutput add) {
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.CABLE.get(), 6)
private void basicRecipes() {
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.CABLE.get(), 6)
.pattern(" # ")
.pattern("#R#")
.pattern(" # ")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_modem", has(WIRED_MODEM))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_NORMAL.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_NORMAL.get())
.pattern("###")
.pattern("#R#")
.pattern("#G#")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.define('G', ingredients.glassPane())
.unlockedBy("has_redstone", inventoryChange(itemPredicate(ingredients.redstone())))
.save(add);
.unlockedBy("has_redstone", has(ingredients.redstone()))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
.pattern("###")
.pattern("#R#")
.pattern("#G#")
.define('#', ingredients.goldIngot())
.define('R', ingredients.redstone())
.define('G', ingredients.glassPane())
.unlockedBy("has_components", inventoryChange(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
.save(add);
.unlockedBy("has_components", inventoryTrigger(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
.save(output);
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
.pattern("###")
.pattern("#C#")
.pattern("# #")
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_COMMAND.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_COMMAND.get())
.pattern("###")
.pattern("#R#")
.pattern("#G#")
.define('#', ingredients.goldIngot())
.define('R', Items.COMMAND_BLOCK)
.define('G', ingredients.glassPane())
.unlockedBy("has_components", inventoryChange(Items.COMMAND_BLOCK))
.save(add);
.unlockedBy("has_components", has(Items.COMMAND_BLOCK))
.save(output);
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_NORMAL.get())
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_NORMAL.get())
.pattern("###")
.pattern("#C#")
.pattern("#I#")
.define('#', ingredients.ironIngot())
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_NORMAL.get()))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
.save(add);
.save(output);
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
.pattern("###")
.pattern("#C#")
.pattern("#I#")
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_NORMAL.get()))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_ADVANCED.get()))))
.save(add);
.save(output);
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
.pattern("###")
.pattern("#C#")
.pattern(" B ")
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.TURTLE_NORMAL.get())
.define('B', ingredients.goldBlock())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.TURTLE_NORMAL.get()))))
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.DISK_DRIVE.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.DISK_DRIVE.get())
.pattern("###")
.pattern("#R#")
.pattern("#R#")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_NORMAL.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_NORMAL.get())
.pattern("###")
.pattern("#G#")
.pattern("###")
.define('#', Items.STONE)
.define('G', ingredients.glassPane())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_ADVANCED.get(), 4)
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_ADVANCED.get(), 4)
.pattern("###")
.pattern("#G#")
.pattern("###")
.define('#', ingredients.goldIngot())
.define('G', ingredients.glassPane())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
.pattern("###")
.pattern("#A#")
.pattern("#G#")
.define('#', Items.STONE)
.define('A', Items.GOLDEN_APPLE)
.define('G', ingredients.glassPane())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_apple", has(Items.GOLDEN_APPLE))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
.pattern("###")
.pattern("#A#")
.pattern("#G#")
.define('#', ingredients.goldIngot())
.define('A', Items.GOLDEN_APPLE)
.define('G', ingredients.glassPane())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_apple", has(Items.GOLDEN_APPLE))
.save(output);
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
.pattern("###")
.pattern("#C#")
.pattern("# #")
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()))))
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTER.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTER.get())
.pattern("###")
.pattern("#R#")
.pattern("#D#")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.define('D', ingredients.dye())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.SPEAKER.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.SPEAKER.get())
.pattern("###")
.pattern("#N#")
.pattern("#R#")
.define('#', Items.STONE)
.define('N', Items.NOTE_BLOCK)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
.pattern("###")
.pattern("#R#")
.pattern("###")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_cable", inventoryChange(ModRegistry.Items.CABLE.get()))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_cable", has(ModRegistry.Items.CABLE.get()))
.save(output);
ShapelessRecipeBuilder
.shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM_FULL.get())
.requires(ModRegistry.Items.WIRED_MODEM.get())
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "wired_modem_full_from"));
ShapelessRecipeBuilder
.shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
.requires(ModRegistry.Items.WIRED_MODEM_FULL.get())
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "wired_modem_full_to"));
oneToOneConversionRecipe(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get(), null);
oneToOneConversionRecipe(ModRegistry.Items.WIRED_MODEM_FULL.get(), ModRegistry.Items.WIRED_MODEM.get(), null);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_NORMAL.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_NORMAL.get())
.pattern("###")
.pattern("#E#")
.pattern("###")
.define('#', Items.STONE)
.define('E', ingredients.enderPearl())
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get())
.pattern("###")
.pattern("#E#")
.pattern("###")
.define('#', ingredients.goldIngot())
.define('E', Items.ENDER_EYE)
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_wireless", inventoryChange(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))
.save(add);
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_wireless", has(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))
.save(output);
ShapelessSpecBuilder
.shapeless(RecipeCategory.DECORATIONS, playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c"))
customShapeless(RecipeCategory.DECORATIONS, playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c"))
.requires(ItemTags.SKULLS)
.requires(ModRegistry.Items.MONITOR_NORMAL.get())
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
.unlockedBy("has_monitor", has(ModRegistry.Items.MONITOR_NORMAL.get()))
.build()
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_cloudy"));
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_cloudy"));
ShapelessSpecBuilder
.shapeless(RecipeCategory.DECORATIONS, playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb"))
customShapeless(RecipeCategory.DECORATIONS, playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb"))
.requires(ItemTags.SKULLS)
.requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
.unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_ADVANCED.get()))
.build()
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_dan200"));
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_dan200"));
var pages = Ingredient.of(
ModRegistry.Items.PRINTED_PAGE.get(),
@@ -477,76 +410,84 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
Items.PAPER
);
ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get())
customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get())
.requires(ingredients.string())
.unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get()))
.unlockedBy("has_printer", has(ModRegistry.Items.PRINTER.get()))
.build(x -> new PrintoutRecipe(x, pages, 2))
.save(add);
.save(output);
ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get())
customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get())
.requires(ingredients.leather())
.requires(ingredients.string())
.unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get()))
.unlockedBy("has_printer", has(ModRegistry.Items.PRINTER.get()))
.build(x -> new PrintoutRecipe(x, pages, 1))
.save(add);
.save(output);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.REDSTONE_RELAY.get())
shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.REDSTONE_RELAY.get())
.pattern("SRS")
.pattern("RCR")
.pattern("SRS")
.define('S', Items.STONE)
.define('R', ingredients.redstone())
.define('C', ModRegistry.Blocks.CABLE.get())
.unlockedBy("has_cable", inventoryChange(ModRegistry.Blocks.CABLE.get()))
.save(add);
.unlockedBy("has_cable", has(ModRegistry.Blocks.CABLE.get()))
.save(output);
}
private static DyeColor ofColour(Colour colour) {
return DyeColor.byId(15 - colour.ordinal());
}
private static Criterion<InventoryChangeTrigger.TriggerInstance> inventoryChange(TagKey<Item> stack) {
return InventoryChangeTrigger.TriggerInstance.hasItems(itemPredicate(stack));
}
private static Criterion<InventoryChangeTrigger.TriggerInstance> inventoryChange(ItemLike... stack) {
return InventoryChangeTrigger.TriggerInstance.hasItems(stack);
}
private static Criterion<InventoryChangeTrigger.TriggerInstance> inventoryChange(ItemPredicate... items) {
private static Criterion<InventoryChangeTrigger.TriggerInstance> has(ItemLike... items) {
return InventoryChangeTrigger.TriggerInstance.hasItems(items);
}
private static ItemPredicate itemPredicate(ItemLike item) {
return ItemPredicate.Builder.item().of(item).build();
private ItemPredicate itemPredicate(ItemLike item) {
return ItemPredicate.Builder.item().of(items, item).build();
}
private static ItemPredicate itemPredicate(TagKey<Item> item) {
return ItemPredicate.Builder.item().of(item).build();
}
private static ItemPredicate itemPredicate(Ingredient ingredient) {
var json = Ingredient.CODEC_NONEMPTY.encodeStart(JsonOps.INSTANCE, ingredient).getOrThrow();
if (!(json instanceof JsonObject object)) throw new IllegalStateException("Unknown ingredient " + json);
if (object.has("item")) {
var item = ItemStack.SIMPLE_ITEM_CODEC.parse(JsonOps.INSTANCE, object).getOrThrow();
return itemPredicate(item.getItem());
} else if (object.has("tag")) {
return itemPredicate(TagKey.create(Registries.ITEM, ResourceLocation.parse(GsonHelper.getAsString(object, "tag"))));
} else {
throw new IllegalArgumentException("Unknown ingredient " + json);
}
private ItemPredicate itemPredicate(TagKey<Item> item) {
return ItemPredicate.Builder.item().of(items, item).build();
}
private static ItemStack playerHead(String name, String uuid) {
return DataComponentUtil.createStack(Items.PLAYER_HEAD, DataComponents.PROFILE, new ResolvableProfile(new GameProfile(UUID.fromString(uuid), name)));
}
private static void addSpecial(RecipeOutput add, Recipe<?> recipe) {
add.accept(RegistryHelper.getKeyOrThrow(BuiltInRegistries.RECIPE_SERIALIZER, recipe.getSerializer()), recipe, null);
private ShapedSpecBuilder customShaped(RecipeCategory category, ItemStack result) {
return new ShapedSpecBuilder(items, category, result);
}
private ShapedSpecBuilder customShaped(RecipeCategory category, ItemLike result) {
return new ShapedSpecBuilder(items, category, new ItemStack(result));
}
private ShapelessSpecBuilder customShapeless(RecipeCategory category, ItemStack result) {
return new ShapelessSpecBuilder(items, category, result);
}
private ShapelessSpecBuilder customShapeless(RecipeCategory category, ItemLike result) {
return new ShapelessSpecBuilder(items, category, new ItemStack(result));
}
private void special(Recipe<?> recipe) {
var key = RegistryHelper.getKeyOrThrow(BuiltInRegistries.RECIPE_SERIALIZER, recipe.getSerializer());
output.accept(recipeKey(key), recipe, null);
}
public static ResourceKey<Recipe<?>> recipeKey(ResourceLocation key) {
return ResourceKey.create(Registries.RECIPE, key);
}
static class Runner extends net.minecraft.data.recipes.RecipeProvider.Runner {
protected Runner(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
super(output, registries);
}
@Override
protected RecipeProvider createRecipeProvider(HolderLookup.Provider registries, RecipeOutput output) {
return new RecipeProvider(registries, output);
}
@Override
public String getName() {
return "Recipes";
}
}
}

View File

@@ -6,6 +6,7 @@ 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;
@@ -23,7 +24,9 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
/**
* Similar to {@link PackMetadataGenerator}, but for individual resources.
* 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;
@@ -40,20 +43,20 @@ final class ResourceMetadataProvider implements DataProvider {
GuiSprites.COMPUTER_NORMAL
)) {
builder.texture(computerTextures.border()).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
new GuiSpriteScaling.NineSlice(36, 36, simpleNineSlicedBorder(12))
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))
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))
new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false)
));
}
}
@@ -103,7 +106,7 @@ final class ResourceMetadataProvider implements DataProvider {
private final Map<String, Supplier<JsonElement>> elements = new HashMap<>();
<T> FileMetadata add(MetadataSectionType<T> type, T value) {
elements.put(type.getMetadataSectionName(), () -> type.toJson(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,9 @@ 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());
@@ -117,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
));

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