1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-16 22:47:38 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
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
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
5fa724ed24 Move Create integration setup to its proper place
Merge conflict gone wrong, I assume.
2025-06-30 22:53:15 +01:00
514 changed files with 8229 additions and 7345 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,7 +4,10 @@
/** Default configuration for Fabric projects. */
import cc.tweaked.gradle.*
import cc.tweaked.gradle.CCTweakedExtension
import cc.tweaked.gradle.CCTweakedPlugin
import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
`java-library`
@@ -64,9 +67,3 @@ 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="^(computercraft\$|\$)?[a-z][a-zA-Z0-9]*$" />
<property name="format" value="^\$?[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.7
mcVersion=1.21.1

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.128.0+1.21.7"
fabric-loader = "0.16.14"
neoForge = "21.7.1-beta"
fabric-api = "0.102.1+1.21.1"
fabric-loader = "0.15.11"
neoForge = "21.1.9"
neoMergeTool = "2.0.0"
mixin = "0.8.5"
parchment = "2025.06.29"
parchmentMc = "1.21.6"
yarn = "1.21.7+build.1"
parchment = "2024.07.28"
parchmentMc = "1.21"
yarn = "1.21.1+build.1"
# Core dependencies (these versions are tied to the version Minecraft uses)
fastutil = "8.5.15"
guava = "33.3.1-jre"
netty = "4.1.118.Final"
slf4j = "2.0.16"
fastutil = "8.5.12"
guava = "32.1.2-jre"
netty = "4.1.97.Final"
slf4j = "2.0.9"
# Core dependencies (independent of Minecraft)
asm = "9.7.1"
asm = "9.6"
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.3"
iris-fabric = "1.8.11+1.21.5-fabric"
iris-forge = "1.8.11+1.21.5-neoforge"
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 = "13.0.2"
modmenu = "11.0.0-rc.4"
moreRed = "6.0.0.3"
rei = "18.0.800"
sodium-fabric = "mc1.21.5-0.6.12-fabric"
sodium-forge = "mc1.21.5-0.6.12-neoforge"
rei = "16.0.729"
sodium-fabric = "mc1.21-0.6.0-beta.1-fabric"
sodium-forge = "mc1.21-0.6.0-beta.1-neoforge"
mixinExtra = "0.3.5"
create-forge = "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.5"
cctJavadoc = "1.8.4"
checkstyle = "10.23.1"
errorProne-core = "2.38.0"
errorProne-plugin = "4.1.0"
@@ -74,7 +74,7 @@ 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"
@@ -186,9 +186,9 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
# Minecraft
externalMods-common = ["iris-forge", "jei-api", "nightConfig-core", "nightConfig-toml"]
externalMods-forge-compile = ["moreRed", "iris-forge", "jei-api"]
externalMods-forge-runtime = []
externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric-compile = ["fabricPermissions", "iris-fabric", "jei-api", "rei-api", "rei-builtin"]
externalMods-fabric-runtime = []
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
# 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

@@ -0,0 +1,84 @@
// 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

@@ -1,166 +0,0 @@
// 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

@@ -0,0 +1,66 @@
// 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

@@ -1,95 +0,0 @@
// 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

@@ -1,143 +0,0 @@
// 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

@@ -1,25 +0,0 @@
// 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

@@ -0,0 +1,26 @@
// 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

@@ -1,209 +0,0 @@
// 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

@@ -1,110 +0,0 @@
// 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

@@ -0,0 +1,112 @@
// 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

@@ -0,0 +1,45 @@
// 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

@@ -0,0 +1,70 @@
// 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

@@ -1,40 +0,0 @@
// 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,6 +11,8 @@ 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;
@@ -140,6 +142,19 @@ 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,7 +6,6 @@ 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;
/**
@@ -21,7 +20,7 @@ public class ComputerComponents {
/**
* The {@link IPocketAccess} associated with a pocket computer.
*/
public static final ComputerComponent<PocketComputer> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
public static final ComputerComponent<IPocketAccess> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
/**
* This component is only present on "command computers", and other computers with admin capabilities.

View File

@@ -0,0 +1,26 @@
// 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,15 +7,60 @@ 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;
/**
* Access to a pocket computer for {@linkplain IPocketUpgrade pocket upgrades}.
* Wrapper class for pocket computers.
*/
@ApiStatus.NonExtendable
public interface IPocketAccess extends PocketComputer {
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);
/**
* Get the colour of this pocket computer's light as a RGB number.
*
@@ -47,8 +92,7 @@ public interface IPocketAccess extends PocketComputer {
/**
* Set the upgrade for this pocket computer, also updating the item stack.
* <p>
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
* active}.
* Note this method is not thread safe - it must be called from the server thread.
*
* @param upgrade The new upgrade to set it to, may be {@code null}.
* @see #getUpgrade()
@@ -70,9 +114,6 @@ public interface IPocketAccess extends PocketComputer {
/**
* 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.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import org.jspecify.annotations.Nullable;
/**
@@ -71,7 +71,7 @@ public interface IPocketUpgrade extends UpgradeBase {
/**
* Called when the pocket computer is right clicked.
*
* @param level The world the computer is in.
* @param world 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(ServerLevel level, IPocketAccess access, @Nullable IPeripheral peripheral) {
default boolean onRightClick(Level world, IPocketAccess access, @Nullable IPeripheral peripheral) {
return false;
}
}

View File

@@ -1,81 +0,0 @@
// 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,6 +51,16 @@ 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}.
@@ -61,15 +71,8 @@ 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>
* 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.
* 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}.
*
* {@snippet class=com.example.examplemod.data.TurtleUpgradeProvider region=body}
*

View File

@@ -4,33 +4,17 @@
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 implements StringRepresentable {
public enum TurtleSide {
/**
* 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");
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;
}
RIGHT,
}

View File

@@ -7,11 +7,8 @@ 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.
@@ -20,9 +17,7 @@ import org.jspecify.annotations.Nullable;
* @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
) implements DataComponentGetter {
public record UpgradeData<T extends UpgradeBase>(Holder.Reference<T> holder, DataComponentPatch data) {
/**
* A utility method to construct a new {@link UpgradeData} instance.
*
@@ -71,17 +66,4 @@ public record UpgradeData<T extends UpgradeBase>(
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 sealed interface UpgradeType<T extends UpgradeBase> permits UpgradeTypeImpl {
public interface UpgradeType<T extends UpgradeBase> {
/**
* The codec to read and write this upgrade from a datapack.
*

View File

@@ -12,6 +12,7 @@ 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;
@@ -59,6 +60,8 @@ 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,13 +11,6 @@ plugins {
id("cc-tweaked.publishing")
}
sourceSets.client {
java {
exclude("dan200/computercraft/client/integration/emi")
exclude("dan200/computercraft/client/integration/jei")
}
}
minecraft {
accessWideners(
"src/main/resources/computercraft.accesswidener",

View File

@@ -6,13 +6,12 @@ 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;
@@ -30,15 +29,16 @@ 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,10 +91,9 @@ public final class ClientHooks {
return false;
}
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);
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);
return true;
}
@@ -136,20 +135,31 @@ public final class ClientHooks {
if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.holder().key().location()));
}
public static BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
/**
* 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) {
// 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 state;
return null;
}
var hit = Minecraft.getInstance().hitResult;
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return state;
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return null;
var hitPos = ((BlockHitResult) hit).getBlockPos();
if (!hitPos.equals(pos)) return state;
if (!hitPos.equals(pos)) return null;
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,53 +4,61 @@
package dan200.computercraft.client;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.*;
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 dan200.computercraft.client.gui.*;
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.pocket.ClientPocketComputers;
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.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.client.turtle.TurtleModemModeller;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.core.util.Colour;
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 net.minecraft.client.color.item.ItemTintSource;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
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.gui.screens.MenuScreens;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
import net.minecraft.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.client.renderer.item.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.ResourceProvider;
import net.minecraft.util.FastColor;
import net.minecraft.world.entity.LivingEntity;
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.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
@@ -61,22 +69,11 @@ import java.util.function.Function;
* @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.
*/
@@ -88,6 +85,25 @@ 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);
@@ -102,102 +118,145 @@ 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 registerTurtleModels(RegisterTurtleUpgradeModel register) {
register.register(BasicUpgradeModel.ID, BasicUpgradeModel.CODEC);
register.register(ItemUpgradeModel.ID, ItemUpgradeModel.CODEC);
register.register(SelectUpgradeModel.ID, SelectUpgradeModel.CODEC);
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);
}
private static final ResourceLocation[] EXTRA_MODELS = {
TurtleOverlay.ELF_MODEL,
TurtleBlockEntityRenderer.NORMAL_TURTLE_MODEL,
TurtleBlockEntityRenderer.ADVANCED_TURTLE_MODEL,
TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
MissingBlockModel.LOCATION,
};
/**
* 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
) {
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);
}
/**
* Gather the list of extra models to load.
*
* @param resources The current resource manager.
* @param executor The executor to schedule loading on.
* @return A promise which contains our extra models.
*/
public static CompletableFuture<ExtraModels> gatherExtraModels(ResourceManager resources, Executor executor) {
var turtleOverlays = TurtleOverlayManager.loader().load(resources, executor);
var turtleUpgrades = TurtleUpgradeModelManager.loader().load(resources, executor);
return turtleOverlays.thenCombine(turtleUpgrades, ExtraModels::new);
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());
}
/**
* A callback used to register a model for a {@link ModelKey}.
*/
public interface RegisterExtraModels {
default <U extends ResolvableModel, T> void register(ModelKey<T> key, U unbaked, BiFunction<U, ModelBaker, T> bake) {
register(key, unbaked, ResolvableModel::resolveDependencies, bake);
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);
}
/**
* Register an extra model.
* <p>
* This accepts functions to resolve dependencies and bake the model. While this would be conceptually nicer as
* an interface, it would require multiple adaptors to convert between "upgrade model", "a"bstract model" and
* "platform-specific model", so working with functions is cleaner.
*
* @param key The model key for this model.
* @param unbaked The unbaked model.
* @param resolve The function to resolve dependencies for this model.
* @param bake The function to bake this model.
* @param <U> The type of unbaked model.
* @param <T> The type of baked model.
*/
<U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake);
}
public static void registerExtraModels(RegisterExtraModels register, ExtraModels models) {
for (var model : EXTRA_MODELS) {
register.register(getModel(model), model, (id, r) -> r.markDependency(id), StandaloneModel::of);
@Deprecated
@Override
public float call(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
return function.unclampedCall(stack, level, entity, layer);
}
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);
/**
* Register client-side commands.
*
* @param dispatcher The dispatcher to register the commands to.
* @param sendError A function to send an error message.
* @param <T> The type of the client-side command context.
*/
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
.then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
.executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
));
}
public static void registerItemColours(BiConsumer<ResourceLocation, MapCodec<? extends ItemTintSource>> register) {
register.accept(PocketComputerLight.ID, PocketComputerLight.CODEC);
}
/**
* Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
*
* @param context The command context.
* @param sendError A function to send an error message.
* @param id The computer's id.
* @param <T> The type of the client-side command context.
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
*/
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
var server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) {
sendError.accept(context, Component.literal("Not on a single-player server"));
return 0;
}
public static void registerSelectItemProperties(BiConsumer<ResourceLocation, SelectItemModelProperty.Type<?, ?>> register) {
register.accept(PocketComputerStateProperty.ID, PocketComputerStateProperty.TYPE);
}
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;
}
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);
Util.getPlatform().openFile(file);
return 1;
}
}

View File

@@ -1,95 +0,0 @@
// 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().getToastManager());
.showOrReplace(minecraft().getToasts());
uploadNagDeadline = Long.MAX_VALUE;
}
}

View File

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

View File

@@ -7,7 +7,6 @@ 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;
@@ -24,7 +23,7 @@ public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
}
@Override

View File

@@ -5,11 +5,9 @@
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.ToastManager;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.gui.components.toasts.ToastComponent;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FormattedCharSequence;
@@ -37,9 +35,8 @@ public class ItemToast implements Toast {
private final Object token;
private final int width;
private boolean changed = true;
private long lastChanged;
private Visibility visibility = Visibility.HIDE;
private boolean isNew = true;
private long firstDisplay;
public ItemToast(Minecraft minecraft, ItemStack stack, Component title, Component message, Object token) {
this.stack = stack;
@@ -51,10 +48,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(ToastManager toasts) {
public void showOrReplace(ToastComponent toasts) {
var existing = toasts.getToast(ItemToast.class, getToken());
if (existing != null) {
existing.changed = true;
existing.isNew = true;
} else {
toasts.addToast(this);
}
@@ -76,22 +73,28 @@ public class ItemToast implements Toast {
}
@Override
public Visibility getWantedVisibility() {
return visibility;
}
public Visibility render(GuiGraphics graphics, ToastComponent component, long time) {
if (isNew) {
@Override
public void update(ToastManager toastManager, long time) {
if (changed) {
lastChanged = time;
changed = false;
firstDisplay = time;
isNew = false;
}
visibility = time - lastChanged < DISPLAY_TIME * toastManager.getNotificationDisplayTimeMultiplier() ? Visibility.SHOW : Visibility.HIDE;
}
@Override
public void render(GuiGraphics graphics, Font font, long time) {
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, width(), height());
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);
}
var textX = MARGIN;
if (!stack.isEmpty()) {
@@ -99,9 +102,23 @@ public class ItemToast implements Toast {
graphics.renderFakeItem(stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE);
}
graphics.drawString(font, title, textX, MARGIN, 0xff500050, false);
graphics.drawString(component.getMinecraft().font, title, textX, MARGIN, 0xff500050, false);
for (var i = 0; i < message.size(); ++i) {
graphics.drawString(font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
graphics.drawString(component.getMinecraft().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,7 +10,6 @@ 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;
@@ -33,8 +32,6 @@ 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;
@@ -70,12 +67,7 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
var direction = scrollHandler.onMouseScroll(scrollX, scrollY);
var inventory = Objects.requireNonNull(minecraft().player).getInventory();
inventory.setSelectedSlot(ScrollWheelHandler.getNextScrollWheelSelection(
direction.y == 0 ? -direction.x : direction.y, inventory.getSelectedSlot(), Inventory.getSelectionSize()
));
Objects.requireNonNull(minecraft().player).getInventory().swapPaint(scrollY);
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
}
@@ -108,7 +100,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, 0xFFFFFFFF, true);
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFF, true);
y += 9;
}
}

View File

@@ -10,7 +10,6 @@ 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;
@@ -88,13 +87,12 @@ public final class OptionScreen extends Screen {
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
// Render the actual texture.
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND,
graphics.blit(BACKGROUND, x, y, 0, 0, innerWidth, PADDING);
graphics.blit(BACKGROUND,
x, y + PADDING, 0, PADDING, innerWidth, innerHeight - PADDING * 2,
innerWidth, PADDING,
256, 256
innerWidth, PADDING
);
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
graphics.blit(BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING);
assertNonNull(messageRenderer).renderLeftAlignedNoShadow(graphics, x + PADDING, y + PADDING, FONT_HEIGHT, 0x404040);
super.render(graphics, mouseX, mouseY, partialTicks);

View File

@@ -7,7 +7,6 @@ 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;
@@ -24,11 +23,9 @@ public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
if (getMenu().isPrinting()) {
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
}
if (getMenu().isPrinting()) graphics.blit(BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45);
}
@Override

View File

@@ -4,37 +4,28 @@
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 PrintoutRenderer
* @see dan200.computercraft.client.render.PrintoutRenderer
*/
public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu> implements ContainerListener {
private PrintoutInfo printout = PrintoutInfo.DEFAULT;
@@ -122,11 +113,13 @@ 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.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()
));
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();
}
@Override
@@ -151,56 +144,4 @@ 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,7 +12,6 @@ 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;
@@ -49,11 +48,8 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
@Override
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
var advanced = family == ComputerFamily.ADVANCED;
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
);
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);
// Render selected slot
var slot = getMenu().getSelectedSlot();
@@ -61,14 +57,14 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
var slotX = slot % 4;
var slotY = slot / 4;
graphics.blitSprite(
RenderPipelines.GUI_TEXTURED, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 22, 22
advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
);
}
// Render sidebar
graphics.blitSprite(
RenderPipelines.GUI_TEXTURED, Nullability.assertNonNull(GuiSprites.getComputerTextures(family).sidebar()),
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,7 +48,10 @@ public class DynamicImageButton extends Button {
setTooltip(message.tooltip());
var texture = this.texture.get(isHoveredOrFocused());
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, texture, getX(), getY(), width, height);
RenderSystem.disableDepthTest();
graphics.blitSprite(texture, getX(), getY(), 0, width, height);
RenderSystem.enableDepthTest();
}
public record HintedMessage(Component message, Tooltip tooltip) {

View File

@@ -4,9 +4,8 @@
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;
@@ -16,16 +15,8 @@ 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;
@@ -264,23 +255,12 @@ public class TerminalWidget extends AbstractWidget {
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
if (!visible) return;
var scissor = graphics.scissorStack.peek();
var terminalPose = new Matrix3x2f(graphics.pose());
var terminalTextures = TextureSetup.singleTextureWithLightmap(graphics.minecraft.getTextureManager().getTexture(FixedWidthFontRenderer.FONT).getTextureView());
var emitter = FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), graphics.bufferSource().getBuffer(RenderTypes.TERMINAL));
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
));
FixedWidthFontRenderer.drawTerminal(
emitter,
(float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
);
}
@Override
@@ -295,49 +275,4 @@ 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,11 +6,12 @@ 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.client.renderer.LightTexture;
import net.minecraft.util.FastColor;
import java.nio.ByteBuffer;
import java.util.Optional;
@@ -30,31 +31,33 @@ public class IrisShaderMod implements ShaderMod.Provider {
}
@Override
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
return new IrisQuadEmitter(quadCount, makeBuffer);
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
return IrisApi.getInstance().getMinorApiRevision() >= 1
? new IrisQuadEmitter(vertexCount, makeBuffer)
: super.getQuadEmitter(vertexCount, makeBuffer);
}
private static final class IrisQuadEmitter extends DirectFixedWidthFontRenderer.QuadEmitter {
private static final class IrisQuadEmitter implements DirectFixedWidthFontRenderer.QuadEmitter {
private final IrisTextVertexSink sink;
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);
private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, makeBuffer);
}
@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,14 +31,16 @@ public class ShaderMod {
}
/**
* Get an appropriate quad emitter for use with a vertex buffer and {@link DirectFixedWidthFontRenderer} .
* Get an appropriate quad emitter for use with {@link DirectVertexBuffer} and {@link DirectFixedWidthFontRenderer} .
*
* @param quadCount The number of quads.
* @param makeBuffer A function to allocate a temporary buffer.
* @param vertexCount The number of vertices.
* @param makeBuffer A function to allocate a temporary buffer.
* @return The quad emitter.
*/
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(makeBuffer.apply(TERMINAL_TEXT.format().getVertexSize() * quadCount * 4));
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(
makeBuffer.apply(RenderTypes.TERMINAL.format().getVertexSize() * vertexCount * 4)
);
}
public interface Provider {

View File

@@ -1,43 +0,0 @@
// 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

@@ -1,65 +0,0 @@
// 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

@@ -1,63 +0,0 @@
// 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

@@ -1,51 +0,0 @@
// 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

@@ -1,46 +0,0 @@
// 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

@@ -0,0 +1,68 @@
// 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(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);
private static final Material MATERIAL_NORMAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_NORMAL);
private static final Material MATERIAL_ADVANCED = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_ADVANCED);
private static final Material MATERIAL_COLOUR = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_COLOUR);
private static final Material MATERIAL_FRAME = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_FRAME);
private static final Material MATERIAL_LIGHT = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_LIGHT);
// The size of the terminal within the model.
public static final float TERM_WIDTH = 12.0f / 32.0f;

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(TextureAtlas.LOCATION_BLOCKS, TEXTURE);
public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE);
private static final int TEXTURE_WIDTH = 32;
private static final int TEXTURE_HEIGHT = 32;

View File

@@ -0,0 +1,92 @@
// 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

@@ -0,0 +1,146 @@
// 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,6 +4,7 @@
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;
@@ -21,13 +22,11 @@ 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;
@@ -35,6 +34,7 @@ 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,14 +62,10 @@ 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) {
level.levelEvent(LevelEvent.SOUND_STOP_JUKEBOX_SONG, pos, 0);
Minecraft.getInstance().levelRenderer.stopJukeboxSongAndNotifyNearby(pos);
} else {
var id = level.registryAccess().lookupOrThrow(Registries.JUKEBOX_SONG).getIdOrThrow(song.value());
level.levelEvent(LevelEvent.SOUND_PLAY_JUKEBOX_SONG, pos, id);
Minecraft.getInstance().levelRenderer.playJukeboxSong(song, pos);
}
}

View File

@@ -4,38 +4,25 @@
package dan200.computercraft.client.platform;
import dan200.computercraft.impl.Services;
import net.minecraft.client.resources.model.ModelDebugName;
import org.jetbrains.annotations.Contract;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.resources.model.BakedModel;
import org.jspecify.annotations.Nullable;
public interface ClientPlatformHelper {
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
static ClientPlatformHelper get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
return (ClientPlatformHelper) dan200.computercraft.impl.client.ClientPlatformHelper.get();
}
/**
* Create a new unique {@link ModelKey}.
* Render a {@link BakedModel}, using any loader-specific hooks.
*
* @param name The debug name for this model key.
* @param <T> The type of baked model.
* @return The newly created model key.
* @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.
*/
@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() {
}
}
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, int @Nullable [] tints);
}

View File

@@ -1,24 +0,0 @@
// 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

@@ -1,47 +0,0 @@
// 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,7 +11,8 @@ 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.ShapeRenderer;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.BlockHitResult;
public final class CableHighlightRenderer {
@@ -26,10 +27,11 @@ 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().level();
var world = camera.getEntity().getCommandSenderWorld();
var state = world.getBlockState(pos);
@@ -47,9 +49,27 @@ public final class CableHighlightRenderer {
var yOffset = pos.getY() - cameraPos.y();
var zOffset = pos.getZ() - cameraPos.z();
BlockOutlineRenderer.render(
bufferSource, (buffer, colour) -> ShapeRenderer.renderShape(transform, buffer, shape, xOffset, yOffset, zOffset, colour)
);
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);
});
return true;
}

View File

@@ -19,10 +19,11 @@ 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.ARGB;
import net.minecraft.util.FastColor;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.phys.Vec3;
@@ -39,16 +40,19 @@ 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, Vec3 camera) {
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) {
poseStack.pushPose();
poseStack.translate(0.5f, 1.0625f, 0.5f);
poseStack.mulPose(Axis.YP.rotationDegrees(-lectern.getBlockState().getValue(LecternBlock.FACING).getClockWise().toYRot()));
@@ -68,7 +72,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
pocketModel.render(
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), DyedItemColor.getOrDefault(item, -1),
ARGB.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
FastColor.ARGB32.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
);
// Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
@@ -78,8 +82,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(FixedWidthFontRenderer.TERMINAL_TEXT));
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(camera, POCKET_TERMINAL_RENDER_DISTANCE)) {
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(RenderTypes.TERMINAL));
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
renderPocketTerminal(poseStack, quadEmitter, terminal);
} else {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);

View File

@@ -1,40 +0,0 @@
// 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

@@ -0,0 +1,98 @@
// 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,10 +14,9 @@ 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.ARGB;
import net.minecraft.util.FastColor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
import org.joml.Matrix4f;
@@ -81,7 +80,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(FixedWidthFontRenderer.TERMINAL_TEXT));
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
if (terminal == null) {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
} else {
@@ -158,20 +157,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(FixedWidthFontRenderer.TERMINAL_TEXT);
var buffer = render.getBuffer(RenderTypes.TERMINAL);
FixedWidthFontRenderer.drawQuad(
FixedWidthFontRenderer.toVertexConsumer(transform, buffer),
width - LIGHT_HEIGHT * 2, height + BORDER / 2.0f, 0.001f, LIGHT_HEIGHT * 2, LIGHT_HEIGHT,
ARGB.opaque(colour), LightTexture.FULL_BRIGHT
FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP
);
}
private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice(
36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12), false
36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12)
);
private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice(
36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false
36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0)
);
private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) {

View File

@@ -9,7 +9,8 @@ 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.client.renderer.entity.state.ItemFrameRenderState;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.decoration.ItemFrame;
import net.minecraft.world.item.ItemStack;
import static dan200.computercraft.client.render.PrintoutRenderer.*;
@@ -33,22 +34,25 @@ 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, PrintoutData.getOrEmpty(stack), stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get(), light);
drawPrintout(transform, render, stack, light);
}
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, PrintoutData data, boolean isBook, int packedLight) {
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, 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.isGlowFrame ? 0xf000d2 : packedLight; // See getLightCoords.
drawPrintout(transform, render, data, isBook, light);
var light = frame.getType() == EntityType.GLOW_ITEM_FRAME ? 0xf000d2 : packedLight; // See getLightVal.
drawPrintout(transform, render, stack, light);
}
private static void drawPrintout(PoseStack transform, MultiBufferSource render, PrintoutData pageData, boolean book, int light) {
private static void drawPrintout(PoseStack transform, MultiBufferSource render, ItemStack stack, int light) {
var pageData = stack.getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
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,8 +11,6 @@ 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;
@@ -25,12 +23,6 @@ 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;
/**
@@ -66,14 +58,11 @@ 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(FixedWidthFontRenderer.TERMINAL_TEXT);
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
for (var line = 0; line < LINES_PER_PAGE && line < text.length; line++) {
FixedWidthFontRenderer.drawString(emitter,
@@ -84,7 +73,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(FixedWidthFontRenderer.TERMINAL_TEXT);
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_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);
@@ -101,7 +90,7 @@ public final class PrintoutRenderer {
var leftPages = page;
var rightPages = pages - page - 1;
var buffer = bufferSource.getBuffer(BACKGROUND);
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_BACKGROUND);
if (isBook) {
// Border
@@ -110,12 +99,12 @@ public final class PrintoutRenderer {
var right = x + X_SIZE + offset - 4;
// Left and right border
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);
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);
// Draw centre panel (just stretched texture, sorry).
drawTexture(matrix, buffer,
x - offset, y, z + BOOK_Z_OFFSET, X_SIZE + offset * 2, Y_SIZE,
x - offset, y, z - 0.02f, X_SIZE + offset * 2, Y_SIZE,
COVER_X + COVER_SIZE / 2.0f, COVER_SIZE, COVER_SIZE, Y_SIZE,
light
);
@@ -123,20 +112,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 + 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);
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);
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 + PAGE_Z_OFFSET, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
drawTexture(matrix, buffer, x, y, z - 1e-3f * 0.5f, 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 + PAGE_Z_OFFSET * (n + 1),
x - offsetAt(n), y, z - 1e-3f * n,
// Use the left "bold" fold for the outermost page
n == leftPages ? 0 : X_FOLD_SIZE, 0,
X_FOLD_SIZE, Y_SIZE, light
@@ -146,7 +135,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 + PAGE_Z_OFFSET * (n + 1),
x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z - 1e-3f * n,
// 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

@@ -0,0 +1,99 @@
// 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,7 +12,6 @@ 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,13 +6,12 @@ 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.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.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
@@ -21,17 +20,15 @@ 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;
@@ -43,7 +40,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
}
@Override
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, Vec3 camera) {
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight) {
transform.pushPose();
// Translate the turtle first, so the label moves with it.
@@ -75,24 +72,31 @@ 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 = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
var overlay = turtle.getOverlay();
if (colour == -1) {
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
// 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);
} else {
// Otherwise render it using the colour item.
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ ARGB.opaque(colour) });
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ FastColor.ARGB32.opaque(colour) });
}
// Render the overlay
if (overlay != null) overlay.model().render(transform, buffers, lightmapCoord, overlayLight);
if (overlay != null) renderModel(transform, buffers, lightmapCoord, overlayLight, overlay.model(), null);
// And the Christmas overlay.
var showChristmas = Holiday.getCurrent() == Holiday.CHRISTMAS && (overlay == null || overlay.showElfOverlay());
var showChristmas = TurtleOverlay.showElfOverlay(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
if (showChristmas) renderModel(transform, buffers, lightmapCoord, overlayLight, TurtleOverlay.ELF_MODEL, null);
// Render the upgrades
@@ -103,7 +107,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.getAccess().getUpgradeWithData(side);
var upgrade = turtle.getUpgrade(side);
if (upgrade == null) return;
transform.pushPose();
@@ -112,15 +116,42 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
transform.translate(0.0f, -0.5f, -0.5f);
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
.renderForLevel(upgrade, side, turtle.getAccess(), transform, buffers, lightmapCoord, overlayLight);
var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
applyTransformation(transform, model.matrix());
renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
transform.popPose();
}
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, int @Nullable [] tints) {
var modelManager = Minecraft.getInstance().getModelManager();
ClientRegistry.getModel(modelManager, modelLocation).render(transform, buffers, lightmapCoord, overlayLight, tints);
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getModelManager();
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), 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,41 +4,45 @@
package dan200.computercraft.client.render.monitor;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.platform.GlStateManager;
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.OptionalDouble;
import java.util.OptionalInt;
import java.util.function.Consumer;
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> {
/**
@@ -49,11 +53,13 @@ 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, Vec3 camera) {
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
// Render from the origin monitor
var originTerminal = monitor.getOriginClientMonitor();
if (originTerminal == null) return;
@@ -70,6 +76,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
return;
}
lastFrame = renderFrame;
renderState.lastRenderFrame = renderFrame;
renderState.lastRenderPos = monitorPos;
@@ -117,7 +124,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
transform.popPose();
} else {
FixedWidthFontRenderer.drawEmptyTerminal(
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
-MARGIN, MARGIN,
(float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2)
);
@@ -129,137 +136,126 @@ 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.vertexBuffer == null) redraw = true;
if (renderState.createBuffer(renderType)) redraw = true;
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);
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);
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
));
var uniformBuffer = getBuffer(MonitorTextureBufferShader.UNIFORM_SIZE);
MonitorTextureBufferShader.setUniformData(uniformBuffer, terminal);
DirectBuffers.setBufferData(GL31.GL_UNIFORM_BUFFER, renderState.tboUniform, uniformBuffer, GL20.GL_STATIC_DRAW);
}
// 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);
}
}
// 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);
renderState.vertexCountAfterBackground = vertexCountAfterBackground;
renderState.vertexCountAfterForeground = vertexCountAfterForeground;
renderState.vertexCountAfterCursor = vertexCountAfterCursor;
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);
});
}
// 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");
}
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;
private static void renderToBuffer(DirectVertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
var sink = ShaderMod.get().getQuadEmitter(size, MonitorBlockEntityRenderer::getBuffer);
var buffer = sink.buffer();
var transforms = RenderSystem.getDynamicUniforms().writeTransform(
RenderSystem.getModelViewMatrix(),
new Vector4f(1.0F, 1.0F, 1.0F, 1.0F),
RenderSystem.getModelOffset(),
RenderSystem.getTextureMatrix(),
RenderSystem.getShaderLineWidth()
);
draw.accept(sink);
buffer.flip();
vbo.upload(buffer.limit() / sink.format().getVertexSize(), RenderTypes.TERMINAL.mode(), sink.format(), buffer);
}
renderType.setupRenderState();
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);
}
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);
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);
}
renderType.clearRenderState();
buffer.clear();
return buffer;
}
@Override
@@ -272,13 +268,27 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
return monitor.getRenderBoundingBox();
}
private static ByteBuffer getBuffer(int capacity) {
var buffer = backingBuffer;
if (buffer == null || buffer.capacity() < capacity) {
buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
}
/**
* Determine if any monitors were rendered this frame.
*
* @return Whether any monitors were rendered.
*/
public static boolean hasRenderedThisFrame() {
return FrameInfo.getRenderFrame() == lastFrame;
}
buffer.clear();
return buffer;
/**
* 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;
}
}

View File

@@ -6,12 +6,13 @@ 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;
@@ -29,10 +30,11 @@ public final class MonitorHighlightRenderer {
// Preserve normal behaviour when crouching.
if (camera.getEntity().isCrouching()) return false;
var world = camera.getEntity().level();
var world = camera.getEntity().getCommandSenderWorld();
var pos = hit.getBlockPos();
if (!(world.getBlockEntity(pos) instanceof MonitorBlockEntity monitor)) return false;
var tile = world.getBlockEntity(pos);
if (!(tile 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);
@@ -47,37 +49,39 @@ public final class MonitorHighlightRenderer {
transformStack.pushPose();
transformStack.translate(pos.getX() - cameraPos.x(), pos.getY() - cameraPos.y(), pos.getZ() - cameraPos.z());
var transform = transformStack.last();
BlockOutlineRenderer.render(bufferSource, (buffer, colour) -> draw(buffer, transform, faces, colour));
// 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);
transformStack.popPose();
return true;
}
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) {
private static void line(VertexConsumer buffer, Matrix4f transform, PoseStack.Pose normal, float x, float y, float z, Direction direction) {
buffer
.addVertex(transform, x, y, z)
.setColor(colour)
.setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
.setColor(0, 0, 0, 0.4f)
.setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
buffer
.addVertex(transform, x + direction.getStepX(), y + direction.getStepY(), z + direction.getStepZ())
.setColor(colour)
.setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
.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());
}
}

View File

@@ -5,10 +5,17 @@
package dan200.computercraft.client.render.monitor;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.platform.GlStateManager;
import dan200.computercraft.client.render.vbo.DirectBuffers;
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraft.core.BlockPos;
import org.jspecify.annotations.Nullable;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.util.HashSet;
import java.util.Set;
@@ -20,39 +27,97 @@ 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 final class MonitorRenderState implements ClientMonitor.RenderState {
public class MonitorRenderState implements ClientMonitor.RenderState {
@GuardedBy("allMonitors")
private static final Set<MonitorRenderState> allMonitors = new HashSet<>();
long lastRenderFrame = -1;
@Nullable
BlockPos lastRenderPos = null;
public long lastRenderFrame = -1;
public @Nullable BlockPos lastRenderPos = null;
@Nullable
GpuBuffer vertexBuffer;
public int tboBuffer;
public int tboTexture;
public int tboUniform;
public @Nullable DirectVertexBuffer backgroundBuffer;
public @Nullable DirectVertexBuffer foregroundBuffer;
int vertexCountAfterBackground;
int vertexCountAfterForeground;
int vertexCountAfterCursor;
/**
* 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;
void register() {
if (vertexBuffer != null) return;
deleteBuffers();
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 (vertexBuffer != null) {
vertexBuffer.close();
vertexBuffer = null;
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;
}
}
@Override
public void close() {
if (vertexBuffer != null) {
if (tboBuffer != 0 || backgroundBuffer != null) {
synchronized (allMonitors) {
allMonitors.remove(this);
}

View File

@@ -0,0 +1,127 @@
// 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,11 +7,12 @@ 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.ARGB;
import net.minecraft.util.FastColor;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
@@ -33,7 +34,7 @@ import static org.lwjgl.system.MemoryUtil.*;
* <p>
* Note this is almost an exact copy of {@link FixedWidthFontRenderer}. While the code duplication is unfortunate,
* it is measurably faster than introducing polymorphism into {@link FixedWidthFontRenderer}.
* <p>
*
* <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
* {@link FixedWidthFontRenderer}.
*/
@@ -156,40 +157,26 @@ 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 abstract static class QuadEmitter {
private int vertexCount;
public interface QuadEmitter {
VertexFormat format();
public abstract ByteBuffer byteBuffer();
ByteBuffer buffer();
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;
}
void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
}
public static final class ByteBufferEmitter extends QuadEmitter {
private final ByteBuffer buffer;
public ByteBufferEmitter(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public ByteBuffer byteBuffer() {
return buffer;
}
public record ByteBufferEmitter(ByteBuffer buffer) implements QuadEmitter {
@Override
public VertexFormat format() {
return TERMINAL_TEXT.format();
return RenderTypes.TERMINAL.format();
}
@Override
@@ -198,24 +185,23 @@ public final class DirectFixedWidthFontRenderer {
}
}
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) {
static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
// Emit a single quad to our buffer. This uses Unsafe (well, LWJGL's MemoryUtil) to directly blit bytes to the
// underlying buffer. This allows us to have a single bounds check up-front, rather than one for every write.
// This provides significant performance gains, at the cost of well, using Unsafe.
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(abgr:BBBB)(uv1:FF)(uv2:SS),
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(rgba: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");
}
var colourAbgr = ARGB.toABGR(colour);
// Pack colour so it is equivalent to abgr:BBBB. This matches the logic in BufferBuilder.
// Pack colour so it is equivalent to rgba:BBBB. This matches the logic in BufferBuilder.
var colourAbgr = FastColor.ABGR32.fromArgb32(colour);
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.ARGB;
import net.minecraft.util.FastColor;
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,13 +35,6 @@ import org.joml.Vector3f;
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;
@@ -49,7 +42,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 = ARGB.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
private static final float Z_OFFSET = 1e-4f;
private FixedWidthFontRenderer() {
@@ -144,7 +137,7 @@ public final class FixedWidthFontRenderer {
var rowY = y + FONT_HEIGHT * i;
drawString(
emitter, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
palette, LightTexture.FULL_BRIGHT
palette, FULL_BRIGHT_LIGHTMAP
);
}
}
@@ -159,12 +152,12 @@ public final class FixedWidthFontRenderer {
// Top and bottom margins
drawBackground(
emitter, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
leftMarginSize, rightMarginSize, topMarginSize, LightTexture.FULL_BRIGHT
leftMarginSize, rightMarginSize, topMarginSize, FULL_BRIGHT_LIGHTMAP
);
drawBackground(
emitter, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
leftMarginSize, rightMarginSize, bottomMarginSize, LightTexture.FULL_BRIGHT
leftMarginSize, rightMarginSize, bottomMarginSize, FULL_BRIGHT_LIGHTMAP
);
// The main text
@@ -172,7 +165,7 @@ public final class FixedWidthFontRenderer {
var rowY = y + FONT_HEIGHT * i;
drawBackground(
emitter, x, rowY, terminal.getBackgroundColourLine(i), palette,
leftMarginSize, rightMarginSize, FONT_HEIGHT, LightTexture.FULL_BRIGHT
leftMarginSize, rightMarginSize, FONT_HEIGHT, FULL_BRIGHT_LIGHTMAP
);
}
}
@@ -188,7 +181,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, LightTexture.FULL_BRIGHT);
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, FULL_BRIGHT_LIGHTMAP);
}
}
@@ -214,7 +207,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, LightTexture.FULL_BRIGHT);
drawQuad(emitter, x, y, 0, width, height, BLACK, FULL_BRIGHT_LIGHTMAP);
}
public record QuadEmitter(Matrix4f poseMatrix, VertexConsumer consumer) {
@@ -227,10 +220,11 @@ 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(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);
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);
}
}

View File

@@ -0,0 +1,71 @@
// 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

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

View File

@@ -0,0 +1,61 @@
// 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

@@ -1,55 +0,0 @@
// 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

@@ -1,42 +0,0 @@
// 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

@@ -1,43 +0,0 @@
// 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

@@ -0,0 +1,67 @@
// 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

@@ -1,30 +0,0 @@
// 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

@@ -2,16 +2,10 @@
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data.client;
package dan200.computercraft.data;
import com.mojang.math.Quadrant;
import com.google.gson.JsonObject;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.item.model.TurtleOverlayModel;
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
@@ -24,22 +18,13 @@ import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.color.item.Dye;
import net.minecraft.client.data.models.BlockModelGenerators;
import net.minecraft.client.data.models.MultiVariant;
import net.minecraft.client.data.models.blockstates.ConditionBuilder;
import net.minecraft.client.data.models.blockstates.MultiPartGenerator;
import net.minecraft.client.data.models.blockstates.MultiVariantGenerator;
import net.minecraft.client.data.models.blockstates.PropertyDispatch;
import net.minecraft.client.data.models.model.*;
import net.minecraft.client.renderer.block.model.VariantMutator;
import net.minecraft.client.renderer.item.EmptyModel;
import net.minecraft.client.renderer.item.properties.conditional.HasComponent;
import net.minecraft.core.Direction;
import net.minecraft.core.component.DataComponents;
import net.minecraft.data.models.BlockModelGenerators;
import net.minecraft.data.models.blockstates.*;
import net.minecraft.data.models.model.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.Property;
@@ -49,11 +34,10 @@ import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import static net.minecraft.client.data.models.BlockModelGenerators.*;
import static net.minecraft.client.data.models.model.ModelLocationUtils.getModelLocation;
import static net.minecraft.client.data.models.model.TextureMapping.getBlockTexture;
import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocation;
import static net.minecraft.data.models.model.TextureMapping.getBlockTexture;
public class BlockModelProvider {
class BlockModelProvider {
private static final TextureSlot CURSOR = TextureSlot.create("cursor");
private static final TextureSlot LEFT = TextureSlot.create("left");
private static final TextureSlot RIGHT = TextureSlot.create("right");
@@ -108,8 +92,6 @@ public class BlockModelProvider {
registerMonitor(generators, ModRegistry.Blocks.MONITOR_ADVANCED.get());
generators.createHorizontallyRotatedBlock(ModRegistry.Blocks.SPEAKER.get(), TexturedModel.ORIENTABLE_ONLY_TOP);
registerSimpleItemModel(generators, ModRegistry.Blocks.SPEAKER.get());
registerDiskDrive(generators);
registerPrinter(generators);
@@ -122,15 +104,16 @@ public class BlockModelProvider {
registerTurtleModem(generators, "block/turtle_modem_normal", "block/wireless_modem_normal_face");
registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
generators.blockStateOutput.accept(
createSimpleBlock(ModRegistry.Blocks.LECTERN.get(), plainVariant(getModelLocation(Blocks.LECTERN)))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(
ModRegistry.Blocks.LECTERN.get(),
Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(Blocks.LECTERN))
).with(createHorizontalFacingDispatch()));
}
private static void registerDiskDrive(BlockModelGenerators generators) {
var diskDrive = ModRegistry.Blocks.DISK_DRIVE.get();
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(diskDrive)
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(diskDrive)
.with(createHorizontalFacingDispatch())
.with(createModelDispatch(DiskDriveBlock.STATE, value -> {
var textureSuffix = switch (value) {
case EMPTY -> "_front";
@@ -143,14 +126,14 @@ public class BlockModelProvider {
generators.modelOutput
);
}))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.registerSimpleItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
generators.delegateItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
}
private static void registerPrinter(BlockModelGenerators generators) {
var printer = ModRegistry.Blocks.PRINTER.get();
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(printer)
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(printer)
.with(createHorizontalFacingDispatch())
.with(createModelDispatch(PrinterBlock.TOP, PrinterBlock.BOTTOM, (top, bottom) -> {
String model, texture;
if (top && bottom) {
@@ -171,13 +154,13 @@ public class BlockModelProvider {
generators.modelOutput
);
}))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.registerSimpleItemModel(printer, getModelLocation(printer, "_empty"));
generators.delegateItemModel(printer, getModelLocation(printer, "_empty"));
}
private static void registerComputer(BlockModelGenerators generators, ComputerBlock<?> block) {
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createHorizontalFacingDispatch())
.with(createModelDispatch(ComputerBlock.STATE, state -> switch (state) {
case OFF -> ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
block, "_" + state.getSerializedName(),
@@ -190,19 +173,11 @@ public class BlockModelProvider {
generators.modelOutput
);
}))
.with(ROTATION_HORIZONTAL_FACING)
);
generators.registerSimpleItemModel(block, getModelLocation(block, "_blinking"));
generators.delegateItemModel(block, getModelLocation(block, "_blinking"));
}
private static void registerTurtle(BlockModelGenerators generators, TurtleBlock block) {
// The actual turtle blockstate is an empty model with just the partcles.
var particleModel = ModelTemplates.PARTICLE_ONLY.createWithSuffix(
block, "_particle", TextureMapping.particle(getBlockTexture(block, "_front")), generators.modelOutput
);
generators.blockStateOutput.accept(createSimpleBlock(block, plainVariant(particleModel)));
// We then register the full model for use in items and the BE renderer.
var model = TURTLE.create(block, new TextureMapping()
.put(TextureSlot.FRONT, getBlockTexture(block, "_front"))
.put(TextureSlot.BACK, getBlockTexture(block, "_back"))
@@ -213,35 +188,31 @@ public class BlockModelProvider {
.put(BACKPACK, getBlockTexture(block, "_backpack")),
generators.modelOutput
);
generators.blockStateOutput.accept(
MultiVariantGenerator.multiVariant(block, Variant.variant().with(VariantProperties.MODEL, model))
.with(createHorizontalFacingDispatch())
);
generators.itemModelOutput.accept(block.asItem(), ItemModelUtils.composite(
ItemModelUtils.conditional(
new HasComponent(DataComponents.DYED_COLOR, false),
ItemModelUtils.tintedModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL, new Dye(-1)),
ItemModelUtils.plainModel(model)
),
new TurtleUpgradeModel.Unbaked(TurtleSide.LEFT, model),
new TurtleUpgradeModel.Unbaked(TurtleSide.RIGHT, model),
new TurtleOverlayModel.Unbaked(model),
ItemModelUtils.isXmas(
ItemModelUtils.conditional(TurtleShowElfOverlay.create(), ItemModelUtils.plainModel(TurtleOverlay.ELF_MODEL), new EmptyModel.Unbaked()),
new EmptyModel.Unbaked()
)
));
generators.modelOutput.accept(getModelLocation(block.asItem()), () -> {
var out = new JsonObject();
out.addProperty("loader", "computercraft:turtle");
out.addProperty("model", model.toString());
return out;
});
}
private static void registerWirelessModem(BlockModelGenerators generators, WirelessModemBlock block) {
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createFacingDispatch())
.with(createModelDispatch(WirelessModemBlock.ON,
on -> modemModel(generators, getModelLocation(block, on ? "_on" : "_off"), getBlockTexture(block, "_face" + (on ? "_on" : "")))
))
.with(ROTATION_FACING));
generators.registerSimpleItemModel(block, getModelLocation(block, "_off"));
)));
generators.delegateItemModel(block, getModelLocation(block, "_off"));
}
private static void registerWiredModems(BlockModelGenerators generators) {
var fullBlock = ModRegistry.Blocks.WIRED_MODEM_FULL.get();
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(fullBlock)
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(fullBlock)
.with(createModelDispatch(WiredModemFullBlock.MODEM_ON, WiredModemFullBlock.PERIPHERAL_ON, (on, peripheral) -> {
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
var faceTexture = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_face" + (peripheral ? "_peripheral" : "") + (on ? "_on" : ""));
@@ -256,8 +227,8 @@ public class BlockModelProvider {
);
})));
generators.registerSimpleItemModel(fullBlock, getModelLocation(fullBlock, "_off"));
generators.registerSimpleItemModel(ModRegistry.Items.WIRED_MODEM.get(), ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_off"));
generators.delegateItemModel(fullBlock, getModelLocation(fullBlock, "_off"));
generators.delegateItemModel(ModRegistry.Items.WIRED_MODEM.get(), ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_off"));
}
private static ResourceLocation modemModel(BlockModelGenerators generators, ResourceLocation name, ResourceLocation texture) {
@@ -288,12 +259,12 @@ public class BlockModelProvider {
monitorModel(generators, block, "_u", 22, 5, 0, 38);
monitorModel(generators, block, "_ud", 21, 6, 0, 37);
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
.with(createModelDispatch(MonitorBlock.STATE, edge -> getModelLocation(block, edge == MonitorEdgeState.NONE ? "" : "_" + edge.getSerializedName())))
.with(ROTATION_HORIZONTAL_FACING)
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createHorizontalFacingDispatch())
.with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
.with(createModelDispatch(MonitorBlock.STATE, edge -> getModelLocation(block, edge == MonitorEdgeState.NONE ? "" : "_" + edge.getSerializedName())))
);
generators.registerSimpleItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
generators.delegateItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
}
private static ResourceLocation monitorModel(BlockModelGenerators generators, MonitorBlock block, String corners, int front, int side, int top, int back) {
@@ -315,54 +286,55 @@ public class BlockModelProvider {
var coreFacing = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_core_facing");
// Up/Down
generator.with(
or(
Condition.or(
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.UP, true),
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.DOWN, true)
),
plainVariant(coreFacing).with(VariantMutator.X_ROT.withValue(Quadrant.R90))
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
);
// North/South and no neighbours
generator.with(
or(
Condition.or(
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST),
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.NORTH, true),
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.SOUTH, true)
),
plainVariant(coreFacing).with(VariantMutator.Y_ROT.withValue(Quadrant.R0))
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R0)
);
// East/West
generator.with(
or(
Condition.or(
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.EAST, true),
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.WEST, true)
),
plainVariant(coreFacing).with(VariantMutator.Y_ROT.withValue(Quadrant.R90))
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R90)
);
// Find all other possibilities and emit a "solid" core which doesn't have a facing direction.
var core = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_core_any");
List<ConditionBuilder> rightAngles = new ArrayList<>();
List<Condition.TerminalCondition> rightAngles = new ArrayList<>();
for (var i = 0; i < DirectionUtil.FACINGS.length; i++) {
for (var j = i; j < DirectionUtil.FACINGS.length; j++) {
if (DirectionUtil.FACINGS[i].getAxis() == DirectionUtil.FACINGS[j].getAxis()) continue;
rightAngles.add(condition()
rightAngles.add(new Condition.TerminalCondition()
.term(CableBlock.CABLE, true).term(CABLE_DIRECTIONS[i], true).term(CABLE_DIRECTIONS[j], true)
);
}
}
generator.with(or(rightAngles.toArray(new ConditionBuilder[0])), plainVariant(core));
generator.with(Condition.or(rightAngles.toArray(new Condition[0])), Variant.variant().with(VariantProperties.MODEL, core));
// Then emit the actual cable arms
var arm = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_arm");
for (var direction : DirectionUtil.FACINGS) {
generator.with(
condition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
plainVariant(arm)
.with(VariantMutator.X_ROT.withValue(toXAngle(direction.getOpposite())))
.with(VariantMutator.Y_ROT.withValue(toYAngle(direction.getOpposite())))
new Condition.TerminalCondition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
Variant.variant()
.with(VariantProperties.MODEL, arm)
.with(VariantProperties.X_ROT, toXAngle(direction.getOpposite()))
.with(VariantProperties.Y_ROT, toYAngle(direction.getOpposite()))
);
}
@@ -372,31 +344,36 @@ public class BlockModelProvider {
for (var peripheral : BOOLEANS) {
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
generator.with(
condition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
plainVariant(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
.with(VariantMutator.X_ROT.withValue(toXAngle(direction)))
.with(VariantMutator.Y_ROT.withValue(toYAngle(direction)))
new Condition.TerminalCondition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
Variant.variant()
.with(VariantProperties.MODEL, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
.with(VariantProperties.X_ROT, toXAngle(direction))
.with(VariantProperties.Y_ROT, toYAngle(direction))
);
}
}
}
generators.blockStateOutput.accept(generator);
generators.registerSimpleItemModel(ModRegistry.Items.CABLE.get(), getModelLocation(ModRegistry.Items.CABLE.get()));
}
private static void registerRedstoneControl(BlockModelGenerators generators) {
var redstoneControl = ModRegistry.Blocks.REDSTONE_RELAY.get();
generators.createHorizontallyRotatedBlock(redstoneControl, TexturedModel.ORIENTABLE);
registerSimpleItemModel(generators, redstoneControl);
var model = ModelTemplates.CUBE_ORIENTABLE_TOP_BOTTOM.create(
redstoneControl, TextureMapping.orientableCube(redstoneControl), generators.modelOutput
);
generators.blockStateOutput.accept(
MultiVariantGenerator.multiVariant(redstoneControl, Variant.variant().with(VariantProperties.MODEL, model))
.with(createHorizontalFacingDispatch())
);
}
private static final BooleanProperty[] CABLE_DIRECTIONS = { CableBlock.DOWN, CableBlock.UP, CableBlock.NORTH, CableBlock.SOUTH, CableBlock.WEST, CableBlock.EAST };
private static final boolean[] BOOLEANS = new boolean[]{ false, true };
private static ConditionBuilder cableNoNeighbour(Direction... directions) {
var condition = condition().term(CableBlock.CABLE, true);
private static Condition.TerminalCondition cableNoNeighbour(Direction... directions) {
var condition = new Condition.TerminalCondition().term(CableBlock.CABLE, true);
for (var direction : directions) condition.term(CABLE_DIRECTIONS[direction.ordinal()], false);
return condition;
}
@@ -419,51 +396,66 @@ public class BlockModelProvider {
registerTurtleUpgrade(generators, name + "_on", texture + "_on");
}
private static void registerSimpleItemModel(BlockModelGenerators generators, Block block) {
generators.registerSimpleItemModel(block, ModelLocationUtils.getModelLocation(block));
}
private static Quadrant toXAngle(Direction direction) {
private static VariantProperties.Rotation toXAngle(Direction direction) {
return switch (direction) {
default -> Quadrant.R0;
case UP -> Quadrant.R270;
case DOWN -> Quadrant.R90;
default -> VariantProperties.Rotation.R0;
case UP -> VariantProperties.Rotation.R270;
case DOWN -> VariantProperties.Rotation.R90;
};
}
private static Quadrant toYAngle(Direction direction) {
private static VariantProperties.Rotation toYAngle(Direction direction) {
return switch (direction) {
default -> Quadrant.R0;
case NORTH -> Quadrant.R0;
case SOUTH -> Quadrant.R180;
case EAST -> Quadrant.R90;
case WEST -> Quadrant.R270;
default -> VariantProperties.Rotation.R0;
case NORTH -> VariantProperties.Rotation.R0;
case SOUTH -> VariantProperties.Rotation.R180;
case EAST -> VariantProperties.Rotation.R90;
case WEST -> VariantProperties.Rotation.R270;
};
}
private static PropertyDispatch<VariantMutator> createVerticalFacingDispatch(Property<Direction> property) {
var dispatch = PropertyDispatch.modify(property);
for (var direction : property.getPossibleValues()) {
dispatch.select(direction, VariantMutator.X_ROT.withValue(toXAngle(direction)));
private static PropertyDispatch createHorizontalFacingDispatch() {
var dispatch = PropertyDispatch.property(BlockStateProperties.HORIZONTAL_FACING);
for (var direction : BlockStateProperties.HORIZONTAL_FACING.getPossibleValues()) {
dispatch.select(direction, Variant.variant().with(VariantProperties.Y_ROT, toYAngle(direction)));
}
return dispatch;
}
private static <T extends Comparable<T>> PropertyDispatch<MultiVariant> createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
var variant = PropertyDispatch.initial(property);
private static PropertyDispatch createVerticalFacingDispatch(Property<Direction> property) {
var dispatch = PropertyDispatch.property(property);
for (var direction : property.getPossibleValues()) {
dispatch.select(direction, Variant.variant().with(VariantProperties.X_ROT, toXAngle(direction)));
}
return dispatch;
}
private static PropertyDispatch createFacingDispatch() {
var dispatch = PropertyDispatch.property(BlockStateProperties.FACING);
for (var direction : BlockStateProperties.FACING.getPossibleValues()) {
dispatch.select(direction, Variant.variant()
.with(VariantProperties.Y_ROT, toYAngle(direction))
.with(VariantProperties.X_ROT, toXAngle(direction))
);
}
return dispatch;
}
private static <T extends Comparable<T>> PropertyDispatch createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
var variant = PropertyDispatch.property(property);
for (var value : property.getPossibleValues()) {
variant.select(value, plainVariant(makeModel.apply(value)));
variant.select(value, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(value)));
}
return variant;
}
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch<MultiVariant> createModelDispatch(
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch createModelDispatch(
Property<T> propertyT, Property<U> propertyU, BiFunction<T, U, ResourceLocation> makeModel
) {
var variant = PropertyDispatch.initial(propertyT, propertyU);
var variant = PropertyDispatch.properties(propertyT, propertyU);
for (var valueT : propertyT.getPossibleValues()) {
for (var valueU : propertyU.getPossibleValues()) {
variant.select(valueT, valueU, plainVariant(makeModel.apply(valueT, valueU)));
variant.select(valueT, valueU, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(valueT, valueU)));
}
}
return variant;

View File

@@ -5,23 +5,18 @@
package dan200.computercraft.data;
import com.mojang.serialization.Codec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.data.client.BlockModelProvider;
import dan200.computercraft.data.client.ItemModelProvider;
import dan200.computercraft.data.client.ExtraModelsProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay;
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;
@@ -29,6 +24,7 @@ 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;
@@ -54,30 +50,33 @@ public final class DataProviders {
var fullRegistryPatch = RegistryPatchGenerator.createLookup(
generator.registries(),
Util.make(new RegistrySetBuilder(), builder -> {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::register);
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
builder.add(IPocketUpgrade.REGISTRY, PocketUpgradeProvider::addUpgrades);
builder.add(TurtleOverlay.REGISTRY, TurtleOverlays::register);
}));
var fullRegistries = fullRegistryPatch.thenApply(RegistrySetBuilder.PatchedRegistries::full);
generator.registries(fullRegistryPatch);
generator.add(out -> new RecipeProvider.Runner(out, fullRegistries));
generator.add(out -> new RecipeProvider(out, fullRegistries));
generator.blockTags(TagProvider::blockTags);
generator.itemTags(TagProvider::itemTags);
var blockTags = generator.blockTags(TagProvider::blockTags);
generator.itemTags(TagProvider::itemTags, blockTags);
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", PackOutput.Target.RESOURCE_PACK, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(AtlasIds.BLOCKS, makeSprites(Stream.of(
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,
LecternPrintoutModel.TEXTURE,
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
)));
out.accept(AtlasIds.GUI, makeSprites(
Stream.of(UpgradeSlot.LEFT_UPGRADE, UpgradeSlot.RIGHT_UPGRADE),
out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites(
// Computers
GuiSprites.COMPUTER_NORMAL.textures(),
GuiSprites.COMPUTER_ADVANCED.textures(),
@@ -88,10 +87,12 @@ public final class DataProviders {
generator.add(ResourceMetadataProvider::new);
generator.addFromCodec("Turtle overlays", PackOutput.Target.RESOURCE_PACK, TurtleOverlay.SOURCE, TurtleOverlay.CODEC, TurtleOverlays::register);
generator.addFromCodec("Turtle upgrade models", PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC, TurtleUpgradeProvider::addModels);
generator.addModels(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
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());
}
});
}
@SafeVarargs
@@ -105,11 +106,11 @@ public final class DataProviders {
<T extends DataProvider> T add(DataProvider.Factory<T> factory);
<T> void addFromCodec(String name, PackOutput.Target target, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
<T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags);
TagsProvider<Item> itemTags(Consumer<TagProvider.TagConsumer<Item>> tags);
TagsProvider<Item> itemTags(Consumer<TagProvider.ItemTagConsumer> tags, TagsProvider<Block> blocks);
/**
* Build new dynamic registries and save them to a pack.
@@ -117,14 +118,5 @@ 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

@@ -0,0 +1,97 @@
// 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,10 +100,8 @@ 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");
@@ -281,18 +279,19 @@ 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.listElements()
BuiltInRegistries.BLOCK.holders()
.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.listElements()
BuiltInRegistries.ITEM.holders()
.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().orElseThrow(), LootTable
add.accept(ModRegistry.Blocks.CABLE.get().getLootTable(), 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().orElseThrow(), LootTable
add.accept(block.getLootTable(), LootTable
.lootTable()
.withPool(LootPool.lootPool()
.setRolls(ConstantValue.exactly(1))

View File

@@ -0,0 +1,101 @@
// 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,11 +4,14 @@
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;
@@ -21,9 +24,11 @@ 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;
@@ -32,7 +37,6 @@ 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;
@@ -40,14 +44,15 @@ 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.world.item.DyeColor;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.item.*;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.item.component.ResolvableProfile;
import net.minecraft.world.item.crafting.CraftingBookCategory;
import net.minecraft.world.item.crafting.Ingredient;
@@ -57,46 +62,68 @@ 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;
private final HolderGetter<Item> items;
private final RecipeIngredients ingredients = PlatformHelper.get().getRecipeIngredients();
RecipeProvider(HolderLookup.Provider registries, RecipeOutput recipeOutput) {
super(registries, recipeOutput);
this.items = registries.lookupOrThrow(Registries.ITEM);
ingredients = PlatformHelper.get().getRecipeIngredients();
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);
}
}
@Override
public void buildRecipes() {
basicRecipes();
diskColours();
pocketUpgrades();
turtleUpgrades();
turtleOverlays();
public void buildRecipes(RecipeOutput add) {
var registries = registries();
special(new ColourableRecipe(CraftingBookCategory.MISC));
special(new ClearColourRecipe(CraftingBookCategory.MISC));
special(new TurtleUpgradeRecipe(CraftingBookCategory.MISC));
special(new PocketComputerUpgradeRecipe(CraftingBookCategory.MISC));
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));
}
/**
* Register a disk recipe.
* Register a crafting recipe for a disk of every dye colour.
*
* @param output The callback to add recipes.
*/
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 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 static List<TurtleItem> turtleItems() {
@@ -105,22 +132,26 @@ 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() {
private void turtleUpgrades(RecipeOutput add, HolderLookup.Provider registries) {
for (var turtleItem : turtleItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
registries.lookupOrThrow(ITurtleUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
var upgrade = upgradeHolder.value();
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
ShapedSpecBuilder
.shaped(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", has(turtleItem, upgrade.getCraftingItem().getItem()))
.unlockedBy("has_items", inventoryChange(turtleItem, upgrade.getCraftingItem().getItem()))
.build(ImpostorShapedRecipe::new)
.save(
output,
add,
name.withSuffix(String.format("/%s/%s", upgradeHolder.key().location().getNamespace(), upgradeHolder.key().location().getPath()))
);
});
@@ -133,40 +164,44 @@ 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() {
private void pocketUpgrades(RecipeOutput add, HolderLookup.Provider registries) {
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();
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
.group(name.toString())
.pattern("#")
.pattern("P")
.define('P', pocket)
.define('#', upgrade.getCraftingItem().getItem())
.unlockedBy("has_items", has(pocket, upgrade.getCraftingItem().getItem()))
.unlockedBy("has_items", inventoryChange(pocket, upgrade.getCraftingItem().getItem()))
.build(ImpostorShapedRecipe::new)
.save(
output,
add,
name.withSuffix(String.format("/%s/%s", upgradeHolder.key().location().getNamespace(), upgradeHolder.key().location().getPath()))
);
});
}
}
private void turtleOverlays() {
turtleOverlay(TurtleOverlays.TRANS_FLAG, x -> x
.unlockedBy("has_dye", has(ingredients.dye()))
private void turtleOverlays(RecipeOutput add, HolderLookup.Provider registries) {
turtleOverlay(add, registries, TurtleOverlays.TRANS_FLAG, x -> x
.unlockedBy("has_dye", inventoryChange(itemPredicate(ingredients.dye())))
.requires(ColourUtils.getDyeTag(DyeColor.LIGHT_BLUE))
.requires(ColourUtils.getDyeTag(DyeColor.PINK))
.requires(ColourUtils.getDyeTag(DyeColor.WHITE))
.requires(Items.STICK)
);
turtleOverlay(TurtleOverlays.RAINBOW_FLAG, x -> x
.unlockedBy("has_dye", has(ingredients.dye()))
turtleOverlay(add, registries, TurtleOverlays.RAINBOW_FLAG, x -> x
.unlockedBy("has_dye", inventoryChange(itemPredicate(ingredients.dye())))
.requires(ColourUtils.getDyeTag(DyeColor.RED))
.requires(ColourUtils.getDyeTag(DyeColor.ORANGE))
.requires(ColourUtils.getDyeTag(DyeColor.YELLOW))
@@ -177,232 +212,264 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
);
}
private void turtleOverlay(ResourceLocation overlay, Consumer<ShapelessSpecBuilder> build) {
private void turtleOverlay(RecipeOutput add, HolderLookup.Provider registries, ResourceKey<TurtleOverlay> overlay, Consumer<ShapelessSpecBuilder> build) {
var holder = registries.lookupOrThrow(overlay.registryKey()).getOrThrow(overlay);
for (var turtleItem : turtleItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), overlay))
var builder = ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), holder))
.group(name.withSuffix("_overlay").toString())
.unlockedBy("has_turtle", has(turtleItem));
.unlockedBy("has_turtle", inventoryChange(turtleItem));
build.accept(builder);
builder
.requires(turtleItem)
.build(s -> new TransformShapelessRecipe(s, List.of(
CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
)))
.save(output, name.withSuffix("_overlays/" + overlay.getPath()));
.save(add, name.withSuffix("_overlays/" + overlay.location().getPath()));
}
}
private void basicRecipes() {
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.CABLE.get(), 6)
private void basicRecipes(RecipeOutput add) {
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.CABLE.get(), 6)
.pattern(" # ")
.pattern("#R#")
.pattern(" # ")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_modem", has(WIRED_MODEM))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_NORMAL.get())
ShapedRecipeBuilder
.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", has(ingredients.redstone()))
.save(output);
.unlockedBy("has_redstone", inventoryChange(itemPredicate(ingredients.redstone())))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
ShapedRecipeBuilder
.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", inventoryTrigger(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
.save(output);
.unlockedBy("has_components", inventoryChange(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
.save(add);
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
ShapedSpecBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
.pattern("###")
.pattern("#C#")
.pattern("# #")
.define('#', ingredients.goldIngot())
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_COMMAND.get())
ShapedRecipeBuilder
.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", has(Items.COMMAND_BLOCK))
.save(output);
.unlockedBy("has_components", inventoryChange(Items.COMMAND_BLOCK))
.save(add);
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_NORMAL.get())
ShapedSpecBuilder
.shaped(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", has(ModRegistry.Items.COMPUTER_NORMAL.get()))
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
.save(output);
.save(add);
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
ShapedSpecBuilder
.shaped(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", has(ModRegistry.Items.COMPUTER_NORMAL.get()))
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_ADVANCED.get()))))
.save(output);
.save(add);
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
ShapedSpecBuilder
.shaped(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", inventoryTrigger(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.TURTLE_NORMAL.get()))))
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.DISK_DRIVE.get())
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.DISK_DRIVE.get())
.pattern("###")
.pattern("#R#")
.pattern("#R#")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_NORMAL.get())
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_NORMAL.get())
.pattern("###")
.pattern("#G#")
.pattern("###")
.define('#', Items.STONE)
.define('G', ingredients.glassPane())
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_ADVANCED.get(), 4)
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_ADVANCED.get(), 4)
.pattern("###")
.pattern("#G#")
.pattern("###")
.define('#', ingredients.goldIngot())
.define('G', ingredients.glassPane())
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
ShapedRecipeBuilder
.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", has(COMPUTER))
.unlockedBy("has_apple", has(Items.GOLDEN_APPLE))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
ShapedRecipeBuilder
.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", has(COMPUTER))
.unlockedBy("has_apple", has(Items.GOLDEN_APPLE))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
.save(add);
customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
ShapedSpecBuilder
.shaped(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", inventoryTrigger(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.unlockedBy("has_components", inventoryChange(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(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTER.get())
ShapedRecipeBuilder
.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", has(COMPUTER))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.SPEAKER.get())
ShapedRecipeBuilder
.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", has(COMPUTER))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
.pattern("###")
.pattern("#R#")
.pattern("###")
.define('#', Items.STONE)
.define('R', ingredients.redstone())
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_cable", has(ModRegistry.Items.CABLE.get()))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_cable", inventoryChange(ModRegistry.Items.CABLE.get()))
.save(add);
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);
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"));
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_NORMAL.get())
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_NORMAL.get())
.pattern("###")
.pattern("#E#")
.pattern("###")
.define('#', Items.STONE)
.define('E', ingredients.enderPearl())
.unlockedBy("has_computer", has(COMPUTER))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get())
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get())
.pattern("###")
.pattern("#E#")
.pattern("###")
.define('#', ingredients.goldIngot())
.define('E', Items.ENDER_EYE)
.unlockedBy("has_computer", has(COMPUTER))
.unlockedBy("has_wireless", has(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))
.save(output);
.unlockedBy("has_computer", inventoryChange(COMPUTER))
.unlockedBy("has_wireless", inventoryChange(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))
.save(add);
customShapeless(RecipeCategory.DECORATIONS, playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c"))
ShapelessSpecBuilder
.shapeless(RecipeCategory.DECORATIONS, playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c"))
.requires(ItemTags.SKULLS)
.requires(ModRegistry.Items.MONITOR_NORMAL.get())
.unlockedBy("has_monitor", has(ModRegistry.Items.MONITOR_NORMAL.get()))
.unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
.build()
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_cloudy"));
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_cloudy"));
customShapeless(RecipeCategory.DECORATIONS, playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb"))
ShapelessSpecBuilder
.shapeless(RecipeCategory.DECORATIONS, playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb"))
.requires(ItemTags.SKULLS)
.requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
.unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_ADVANCED.get()))
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
.build()
.save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_dan200"));
.save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_dan200"));
var pages = Ingredient.of(
ModRegistry.Items.PRINTED_PAGE.get(),
@@ -410,84 +477,76 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
Items.PAPER
);
customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get())
ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get())
.requires(ingredients.string())
.unlockedBy("has_printer", has(ModRegistry.Items.PRINTER.get()))
.unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get()))
.build(x -> new PrintoutRecipe(x, pages, 2))
.save(output);
.save(add);
customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get())
ShapelessSpecBuilder
.shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get())
.requires(ingredients.leather())
.requires(ingredients.string())
.unlockedBy("has_printer", has(ModRegistry.Items.PRINTER.get()))
.unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get()))
.build(x -> new PrintoutRecipe(x, pages, 1))
.save(output);
.save(add);
shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.REDSTONE_RELAY.get())
ShapedRecipeBuilder
.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", has(ModRegistry.Blocks.CABLE.get()))
.save(output);
.unlockedBy("has_cable", inventoryChange(ModRegistry.Blocks.CABLE.get()))
.save(add);
}
private static Criterion<InventoryChangeTrigger.TriggerInstance> has(ItemLike... items) {
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) {
return InventoryChangeTrigger.TriggerInstance.hasItems(items);
}
private ItemPredicate itemPredicate(ItemLike item) {
return ItemPredicate.Builder.item().of(items, item).build();
private static ItemPredicate itemPredicate(ItemLike item) {
return ItemPredicate.Builder.item().of(item).build();
}
private ItemPredicate itemPredicate(TagKey<Item> 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 static ItemStack playerHead(String name, String uuid) {
return DataComponentUtil.createStack(Items.PLAYER_HEAD, DataComponents.PROFILE, new ResolvableProfile(new GameProfile(UUID.fromString(uuid), name)));
}
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";
}
private static void addSpecial(RecipeOutput add, Recipe<?> recipe) {
add.accept(RegistryHelper.getKeyOrThrow(BuiltInRegistries.RECIPE_SERIALIZER, recipe.getSerializer()), recipe, null);
}
}

View File

@@ -6,7 +6,6 @@ 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;
@@ -24,9 +23,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
/**
* Generates {@code .mcmeta} files for texture files.
* <p>
* This is similar to {@link PackMetadataGenerator}, but for individual resources.
* Similar to {@link PackMetadataGenerator}, but for individual resources.
*/
final class ResourceMetadataProvider implements DataProvider {
private final PackOutput output;
@@ -43,20 +40,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), false)
new GuiSpriteScaling.NineSlice(36, 36, simpleNineSlicedBorder(12))
));
var sidebar = computerTextures.sidebar();
if (sidebar != null) {
builder.texture(sidebar).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
new GuiSpriteScaling.NineSlice(17, 14, new GuiSpriteScaling.NineSlice.Border(3, 4, 0, 3), false)
new GuiSpriteScaling.NineSlice(17, 14, new GuiSpriteScaling.NineSlice.Border(3, 4, 0, 3))
));
}
var pocketBottom = computerTextures.pocketBottom();
if (pocketBottom != null) {
builder.texture(pocketBottom).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false)
new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0))
));
}
}
@@ -106,7 +103,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.name(), () -> type.codec().encodeStart(JsonOps.INSTANCE, value).getOrThrow().getAsJsonObject());
elements.put(type.getMetadataSectionName(), () -> type.toJson(value));
return this;
}
}

View File

@@ -7,10 +7,13 @@ package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.integration.ExternalModTags;
import net.minecraft.data.tags.TagAppender;
import dan200.computercraft.shared.util.RegistryHelper;
import net.minecraft.core.Registry;
import net.minecraft.data.tags.ItemTagsProvider;
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;
@@ -25,8 +28,14 @@ import net.minecraft.world.level.block.Blocks;
*/
class TagProvider {
public static void blockTags(TagConsumer<Block> tags) {
itemAndBlockTags((b, i) -> tags.tag(b));
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());
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);
@@ -82,9 +91,11 @@ class TagProvider {
);
}
public static void itemTags(TagConsumer<Item> tags) {
itemAndBlockTags((b, i) -> tags.tag(i).map(Block::asItem));
public static void itemTags(ItemTagConsumer tags) {
tags.copy(ComputerCraftTags.Blocks.COMPUTER, ComputerCraftTags.Items.COMPUTER);
tags.copy(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE);
tags.tag(ComputerCraftTags.Items.WIRED_MODEM).add(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get());
tags.copy(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR);
tags.tag(ComputerCraftTags.Items.DISKS).add(ModRegistry.Items.DISK.get(), ModRegistry.Items.TREASURE_DISK.get());
tags.tag(ComputerCraftTags.Items.POCKET_COMPUTERS).add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
@@ -106,32 +117,37 @@ 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, T> tag(TagKey<T> tag);
TagAppender<T> tag(TagKey<T> tag);
}
private interface BlockItemTagConsumer {
TagAppender<Block, ?> tag(TagKey<Block> blockTag, TagKey<Item> itemTag);
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);
}
}

View File

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

View File

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

View File

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

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