mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-15 14:07:38 +00:00
Compare commits
86 Commits
v1.21.4-1.
...
v1.21.8-1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4cccf1817c | ||
![]() |
b8d9499027 | ||
![]() |
e81a2c72ce | ||
![]() |
01de6110c6 | ||
![]() |
9cf0f85fcb | ||
![]() |
018ce7c8a5 | ||
![]() |
44726827b4 | ||
![]() |
2bf0aba455 | ||
![]() |
3cf914cb4c | ||
![]() |
4868c4aa32 | ||
![]() |
180156ff1c | ||
![]() |
c6ba753568 | ||
![]() |
9f45c91925 | ||
![]() |
67412a2b72 | ||
![]() |
5fa724ed24 | ||
![]() |
76869593f0 | ||
![]() |
fbf994e803 | ||
![]() |
8344c0a5c2 | ||
![]() |
a292d33830 | ||
![]() |
341d1c7bc2 | ||
![]() |
846f9dff03 | ||
![]() |
64d10ad45b | ||
![]() |
531eacfac7 | ||
![]() |
1f3da5205c | ||
![]() |
e3fecb013a | ||
![]() |
798ceefafe | ||
![]() |
7c0f79fc3c | ||
![]() |
b35cefc5dd | ||
![]() |
ec3dd328b3 | ||
![]() |
f3f43191ab | ||
![]() |
89dd521930 | ||
![]() |
9272e2efcd | ||
![]() |
69353a4fcf | ||
![]() |
ff363dca5a | ||
![]() |
1c51282426 | ||
![]() |
4a3a1c9275 | ||
![]() |
2557dd0af9 | ||
![]() |
b5c0c6e104 | ||
![]() |
876fd8ddb8 | ||
![]() |
ee3b1343b5 | ||
![]() |
b440b964b7 | ||
![]() |
5dfc401b45 | ||
![]() |
0790a8346a | ||
![]() |
418c9be7ac | ||
![]() |
b491f6b11f | ||
![]() |
acafc06449 | ||
![]() |
598fd98a8b | ||
![]() |
e13e8ff92e | ||
![]() |
0a0c80db41 | ||
![]() |
4344c3072f | ||
![]() |
c20336286b | ||
![]() |
356366ede8 | ||
![]() |
a1df196673 | ||
![]() |
947001104d | ||
![]() |
8711512769 | ||
![]() |
a939ad8b97 | ||
![]() |
fdae94b3c1 | ||
![]() |
9c0ce27ce6 | ||
![]() |
c458360b18 | ||
![]() |
09ad6c1905 | ||
![]() |
0e1e8a72d3 | ||
![]() |
995a6e7379 | ||
![]() |
ffa6eadc26 | ||
![]() |
7c1e8e1951 | ||
![]() |
b805a34c2d | ||
![]() |
b03546a158 | ||
![]() |
582713467f | ||
![]() |
b6f41a0df5 | ||
![]() |
594738a022 | ||
![]() |
27f2ab364c | ||
![]() |
5a43273757 | ||
![]() |
97e28516fb | ||
![]() |
676fb5fb53 | ||
![]() |
08dc08b5a3 | ||
![]() |
8f4d4038f6 | ||
![]() |
63ba3fe274 | ||
![]() |
749b3df227 | ||
![]() |
b97634b717 | ||
![]() |
8ade1c38ac | ||
![]() |
1b8344d0a3 | ||
![]() |
b42bc0a01a | ||
![]() |
70a7478529 | ||
![]() |
0cff73e2fc | ||
![]() |
05163a4911 | ||
![]() |
a892739f8e | ||
![]() |
f8785a092f |
@@ -27,7 +27,7 @@ indent_size = 2
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[{*.kt,*.kts}]
|
||||
[*.{kt,kts}]
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_size = 4
|
||||
ij_kotlin_spaces_around_equality_operators = true
|
||||
@@ -39,3 +39,14 @@ ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
ij_kotlin_method_parameters_wrap = off
|
||||
ij_kotlin_call_parameters_wrap = off
|
||||
ij_kotlin_extends_list_wrap = off
|
||||
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_class-naming = disabled
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_function-naming = disabled
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
|
||||
# FIXME: These two are disable right now as they're over-eager in putting things
|
||||
# on the same line. We should set max_line_length and handle this properly.
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
|
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -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
|
||||
|
@@ -88,8 +88,8 @@ You'll first need to [set up a development environment as above](#setting-up-a-d
|
||||
|
||||
Once this is set up, you can now run `./gradlew docWebsite`. This generates documentation from our Lua and Java code,
|
||||
writing the resulting HTML into `./projects/web/build/site`, which can then be opened in a browser. When iterating on
|
||||
documentation, you can instead run `./gradlew docWebsite -t`, which will rebuild documentation every time you change a
|
||||
file.
|
||||
documentation, you can instead run `./gradlew :web:assemble -x :web:compileTeaVM -t`, which will rebuild documentation
|
||||
every time you change a file.
|
||||
|
||||
Documentation is built using [illuaminate] which, while not currently documented (somewhat ironic), is largely the same
|
||||
as [ldoc][ldoc]. Documentation comments are written in Markdown, though note that we do not support many GitHub-specific
|
||||
|
@@ -68,5 +68,5 @@ tasks.ideaSyncTask {
|
||||
tasks.named("checkDependencyConsistency", DependencyCheck::class.java) {
|
||||
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
// Minecraft depends on asm, but Fabric forces it to a more recent version
|
||||
override(libs.findLibrary("asm").get(), "9.7.1")
|
||||
override(libs.findLibrary("asm").get(), "9.8")
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@
|
||||
|
||||
import cc.tweaked.gradle.CCTweakedExtension
|
||||
import cc.tweaked.gradle.CCTweakedPlugin
|
||||
import cc.tweaked.gradle.IdeaRunConfigurations
|
||||
import cc.tweaked.gradle.MinecraftConfigurations
|
||||
|
||||
plugins {
|
||||
|
@@ -29,7 +29,7 @@ base.archivesName.convention("cc-tweaked-$mcVersion-${project.name}")
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion= CCTweakedPlugin.JAVA_VERSION
|
||||
languageVersion = CCTweakedPlugin.JAVA_VERSION
|
||||
}
|
||||
|
||||
withSourcesJar()
|
||||
@@ -93,6 +93,7 @@ sourceSets.all {
|
||||
check("InvalidBlockTag", CheckSeverity.OFF) // Broken by @cc.xyz
|
||||
check("InlineMeSuggester", CheckSeverity.OFF) // Minecraft uses @Deprecated liberally
|
||||
// Too many false positives right now. Maybe we need an indirection for it later on.
|
||||
check("AssignmentExpression", CheckSeverity.OFF) // I'm a bad person.
|
||||
check("ReferenceEquality", CheckSeverity.OFF)
|
||||
check("EnumOrdinal", CheckSeverity.OFF) // For now. We could replace most of these with EnumMap.
|
||||
check("OperatorPrecedence", CheckSeverity.OFF) // For now.
|
||||
@@ -121,7 +122,6 @@ tasks.compileTestJava {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tasks.withType(JavaCompile::class.java).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
@@ -170,7 +170,7 @@ tasks.test {
|
||||
|
||||
tasks.withType(JacocoReport::class.java).configureEach {
|
||||
reports.xml.required = true
|
||||
reports.html.required =true
|
||||
reports.html.required = true
|
||||
}
|
||||
|
||||
project.plugins.withType(CCTweakedPlugin::class.java) {
|
||||
@@ -194,30 +194,23 @@ spotless {
|
||||
fun FormatExtension.defaults() {
|
||||
endWithNewline()
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces(4)
|
||||
leadingTabsToSpaces(4)
|
||||
}
|
||||
|
||||
java {
|
||||
defaults()
|
||||
importOrder("", "javax|java", "\\#")
|
||||
removeUnusedImports()
|
||||
}
|
||||
|
||||
val ktlintConfig = mapOf(
|
||||
"ktlint_standard_no-wildcard-imports" to "disabled",
|
||||
"ktlint_standard_class-naming" to "disabled",
|
||||
"ktlint_standard_function-naming" to "disabled",
|
||||
"ij_kotlin_allow_trailing_comma" to "true",
|
||||
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
|
||||
)
|
||||
|
||||
kotlinGradle {
|
||||
defaults()
|
||||
ktlint().editorConfigOverride(ktlintConfig)
|
||||
ktlint()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
defaults()
|
||||
ktlint().editorConfigOverride(ktlintConfig)
|
||||
ktlint()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,10 +5,8 @@
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.AbstractExecTask
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import java.io.File
|
||||
|
||||
abstract class ExecToDir : AbstractExecTask<ExecToDir>(ExecToDir::class.java) {
|
||||
@get:OutputDirectory
|
||||
|
@@ -25,7 +25,6 @@ import javax.xml.xpath.XPathFactory
|
||||
* Would be good to PR some (or all) of these changes upstream at some point.
|
||||
*
|
||||
* @see net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask
|
||||
* @see net.minecraftforge.gradle.common.util.runs.IntellijRunGenerator
|
||||
*/
|
||||
internal class IdeaRunConfigurations(project: Project) {
|
||||
private val rootProject = project.rootProject
|
||||
@@ -35,22 +34,6 @@ internal class IdeaRunConfigurations(project: Project) {
|
||||
private val writer = TransformerFactory.newInstance().newTransformer()
|
||||
|
||||
private val ideaDir = rootProject.file(".idea/")
|
||||
private val buildDir: Lazy<String?> = lazy {
|
||||
val ideaMisc = ideaDir.resolve("misc.xml")
|
||||
|
||||
try {
|
||||
val doc = Files.newBufferedReader(ideaMisc.toPath()).use {
|
||||
documentBuilder.parse(InputSource(it))
|
||||
}
|
||||
val node =
|
||||
xpath.evaluate("//component[@name=\"ProjectRootManager\"]/output", doc, XPathConstants.NODE) as Node
|
||||
val attr = node.attributes.getNamedItem("url") as Attr
|
||||
attr.value.removePrefix("file://")
|
||||
} catch (e: Exception) {
|
||||
LOGGER.error("Failed to find root directory", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun patch() = synchronized(LOCK) {
|
||||
val runConfigDir = ideaDir.resolve("runConfigurations")
|
||||
@@ -58,10 +41,9 @@ internal class IdeaRunConfigurations(project: Project) {
|
||||
|
||||
Files.list(runConfigDir.toPath()).use {
|
||||
for (configuration in it) {
|
||||
val filename = configuration.fileName.toString();
|
||||
val filename = configuration.fileName.toString()
|
||||
when {
|
||||
filename.endsWith("_fabric.xml") -> patchFabric(configuration)
|
||||
filename.startsWith("forge_") && filename.endsWith(".xml") -> patchForge(configuration)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -72,65 +54,6 @@ internal class IdeaRunConfigurations(project: Project) {
|
||||
setXml("//configuration", "folderName") { "Fabric" }
|
||||
}
|
||||
|
||||
private fun patchForge(path: Path) = withXml(path) {
|
||||
val configId = path.fileName.toString().removePrefix("forge_").removeSuffix(".xml")
|
||||
val sourceSet = forgeConfigs[configId]
|
||||
if (sourceSet == null) {
|
||||
LOGGER.error("[{}] Cannot map run configuration to a known source set", path)
|
||||
return@withXml
|
||||
}
|
||||
|
||||
setXml("//configuration", "folderName") { "Forge" }
|
||||
setXml("//configuration/module", "name") { "${rootProject.name}.forge.$sourceSet" }
|
||||
|
||||
if (buildDir.value == null) return@withXml
|
||||
setXml("//configuration/envs/env[@name=\"MOD_CLASSES\"]", "value") { classpath ->
|
||||
val classes = classpath!!.split(':')
|
||||
val newClasses = mutableListOf<String>()
|
||||
fun appendUnique(x: String) {
|
||||
if (!newClasses.contains(x)) newClasses.add(x)
|
||||
}
|
||||
|
||||
for (entry in classes) {
|
||||
if (!entry.contains("/out/")) {
|
||||
appendUnique(entry)
|
||||
continue
|
||||
}
|
||||
|
||||
val match = CLASSPATH_ENTRY.matchEntire(entry)
|
||||
if (match != null) {
|
||||
val modId = match.groups["modId"]!!.value
|
||||
val proj = match.groups["proj"]!!.value
|
||||
var component = match.groups["component"]!!.value
|
||||
if (component == "production") component = "main"
|
||||
|
||||
appendUnique(forgeModEntry(modId, proj, component))
|
||||
} else {
|
||||
LOGGER.warn("[{}] Unknown classpath entry {}", path, entry)
|
||||
appendUnique(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure common code is on the classpath
|
||||
for (proj in listOf("common", "common-api")) {
|
||||
for (component in listOf("main", "client")) {
|
||||
appendUnique(forgeModEntry("computercraft", proj, component))
|
||||
}
|
||||
}
|
||||
|
||||
if (newClasses.any { it.startsWith("cctest%%") }) {
|
||||
appendUnique(forgeModEntry("cctest", "core", "testFixtures"))
|
||||
appendUnique(forgeModEntry("cctest", "common", "testFixtures"))
|
||||
appendUnique(forgeModEntry("cctest", "common", "testMod"))
|
||||
}
|
||||
|
||||
newClasses.joinToString(":")
|
||||
}
|
||||
}
|
||||
|
||||
private fun forgeModEntry(mod: String, project: String, component: String) =
|
||||
"$mod%%${buildDir.value}/production/${rootProject.name}.$project.$component"
|
||||
|
||||
private fun LocatedDocument.setXml(xpath: String, attribute: String, value: (String?) -> String) {
|
||||
val node = this@IdeaRunConfigurations.xpath.evaluate(xpath, document, XPathConstants.NODE) as Node?
|
||||
if (node == null) {
|
||||
@@ -159,16 +82,5 @@ internal class IdeaRunConfigurations(project: Project) {
|
||||
companion object {
|
||||
private val LOGGER = Logging.getLogger(IdeaRunConfigurations::class.java)
|
||||
private val LOCK = Any()
|
||||
|
||||
private val CLASSPATH_ENTRY =
|
||||
Regex("(?<modId>[a-z]+)%%\\\$PROJECT_DIR\\\$/projects/(?<proj>[a-z-]+)/out/(?<component>\\w+)/(?<type>[a-z]+)\$")
|
||||
|
||||
private val forgeConfigs = mapOf(
|
||||
"runClient" to "client",
|
||||
"runData" to "main",
|
||||
"runGameTestServer" to "testMod",
|
||||
"runServer" to "main",
|
||||
"runTestClient" to "testMod",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import groovy.util.Node
|
||||
import groovy.util.NodeList
|
||||
|
||||
object XmlUtil {
|
||||
fun findChild(node: Node, name: String): Node? = when (val child = node.get(name)) {
|
||||
is Node -> child
|
||||
is NodeList -> child.singleOrNull() as Node?
|
||||
else -> null
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false
|
||||
|
||||
# Mod properties
|
||||
isUnstable=true
|
||||
modVersion=1.115.1
|
||||
modVersion=1.116.1
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.21.4
|
||||
mcVersion=1.21.8
|
||||
|
@@ -7,26 +7,26 @@
|
||||
# Minecraft
|
||||
# MC version is specified in gradle.properties, as we need that in settings.gradle.
|
||||
# Remember to update corresponding versions in fabric.mod.json/neoforge.mods.toml
|
||||
fabric-api = "0.118.0+1.21.4"
|
||||
fabric-loader = "0.16.10"
|
||||
neoForge = "21.4.101-beta"
|
||||
neoForgeSpi = "8.0.1"
|
||||
fabric-api = "0.129.0+1.21.8"
|
||||
fabric-loader = "0.16.14"
|
||||
neoForge = "21.8.0-beta"
|
||||
neoMergeTool = "2.0.0"
|
||||
mixin = "0.8.5"
|
||||
parchment = "2024.12.07"
|
||||
parchmentMc = "1.21.4"
|
||||
yarn = "1.21.4+build.1"
|
||||
parchment = "2025.06.29"
|
||||
parchmentMc = "1.21.6"
|
||||
yarn = "1.21.7+build.1"
|
||||
|
||||
# Core dependencies (these versions are tied to the version Minecraft uses)
|
||||
fastutil = "8.5.15"
|
||||
guava = "33.3.1-jre"
|
||||
netty = "4.1.115.Final"
|
||||
netty = "4.1.118.Final"
|
||||
slf4j = "2.0.16"
|
||||
|
||||
# Core dependencies (independent of Minecraft)
|
||||
asm = "9.7.1"
|
||||
autoService = "1.1.1"
|
||||
checkerFramework = "3.42.0"
|
||||
cobalt = { strictly = "0.9.5" }
|
||||
cobalt = { strictly = "0.9.6" }
|
||||
commonsCli = "1.6.0"
|
||||
jetbrainsAnnotations = "24.1.0"
|
||||
jspecify = "1.0.0"
|
||||
@@ -38,14 +38,14 @@ nightConfig = "3.8.1"
|
||||
# Minecraft mods
|
||||
emi = "1.1.7+1.21"
|
||||
fabricPermissions = "0.3.3"
|
||||
iris-fabric = "1.8.8+1.21.4-fabric"
|
||||
iris-forge = "1.8.8+1.21.4-neoforge"
|
||||
jei = "19.8.2.99"
|
||||
iris-fabric = "1.9.1+1.21.7-fabric"
|
||||
iris-forge = "1.9.1+1.21.7-neoforge"
|
||||
jei = "23.1.0.4"
|
||||
modmenu = "13.0.2"
|
||||
moreRed = "6.0.0.3"
|
||||
rei = "18.0.800"
|
||||
sodium-fabric = "mc1.21.4-0.6.10-fabric"
|
||||
sodium-forge = "mc1.21.4-0.6.10-neoforge"
|
||||
sodium-fabric = "mc1.21.6-0.6.13-fabric"
|
||||
sodium-forge = "mc1.21.6-0.6.13-neoforge"
|
||||
mixinExtra = "0.3.5"
|
||||
create-forge = "6.0.0-6"
|
||||
create-fabric = "0.5.1-f-build.1467+mc1.20.1"
|
||||
@@ -58,24 +58,24 @@ junitPlatform = "1.11.4"
|
||||
jmh = "1.37"
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = "1.8.3"
|
||||
checkstyle = "10.21.2"
|
||||
errorProne-core = "2.36.0"
|
||||
cctJavadoc = "1.8.5"
|
||||
checkstyle = "10.23.1"
|
||||
errorProne-core = "2.38.0"
|
||||
errorProne-plugin = "4.1.0"
|
||||
fabric-loom = "1.9.2"
|
||||
fabric-loom = "1.10.4"
|
||||
githubRelease = "2.5.2"
|
||||
gradleVersions = "0.50.0"
|
||||
ideaExt = "1.1.7"
|
||||
illuaminate = "0.1.0-74-gf1551d5"
|
||||
illuaminate = "0.1.0-83-g1131f68"
|
||||
lwjgl = "3.3.3"
|
||||
minotaur = "2.8.7"
|
||||
modDevGradle = "2.0.74"
|
||||
nullAway = "0.12.3"
|
||||
modDevGradle = "2.0.99"
|
||||
nullAway = "0.12.7"
|
||||
shadow = "8.3.1"
|
||||
spotless = "6.23.3"
|
||||
spotless = "7.0.2"
|
||||
taskTree = "2.1.1"
|
||||
teavm = "0.11.0-SQUID.1"
|
||||
vanillaExtract = "0.2.0"
|
||||
teavm = "0.13.0-SQUID.1"
|
||||
vanillaExtract = "0.2.1"
|
||||
versionCatalogUpdate = "0.8.1"
|
||||
|
||||
[libraries]
|
||||
@@ -87,7 +87,7 @@ checkerFramework = { module = "org.checkerframework:checker-qual", version.ref =
|
||||
cobalt = { module = "cc.tweaked:cobalt", version.ref = "cobalt" }
|
||||
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
|
||||
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
|
||||
neoForgeSpi = { module = "net.neoforged:neoforgespi", version.ref = "neoForgeSpi" }
|
||||
neoMergeTool = { module = "net.neoforged:mergetool", version.ref = "neoMergeTool" }
|
||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||
jetbrainsAnnotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" }
|
||||
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
|
||||
@@ -113,9 +113,9 @@ fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-l
|
||||
fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" }
|
||||
iris-fabric = { module = "maven.modrinth:iris", version.ref = "iris-fabric" }
|
||||
iris-forge = { module = "maven.modrinth:iris", version.ref = "iris-forge" }
|
||||
jei-api = { module = "mezz.jei:jei-1.21-common-api", version.ref = "jei" }
|
||||
jei-fabric = { module = "mezz.jei:jei-1.21-fabric", version.ref = "jei" }
|
||||
jei-forge = { module = "mezz.jei:jei-1.21-neoforge", version.ref = "jei" }
|
||||
jei-api = { module = "mezz.jei:jei-1.21.7-common-api", version.ref = "jei" }
|
||||
jei-fabric = { module = "mezz.jei:jei-1.21.7-fabric", version.ref = "jei" }
|
||||
jei-forge = { module = "mezz.jei:jei-1.21.7-neoforge", version.ref = "jei" }
|
||||
mixin = { module = "org.spongepowered:mixin", version.ref = "mixin" }
|
||||
mixinExtra = { module = "io.github.llamalad7:mixinextras-common", version.ref = "mixinExtra" }
|
||||
modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" }
|
||||
@@ -186,7 +186,7 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
|
||||
# Minecraft
|
||||
externalMods-common = ["iris-forge", "jei-api", "nightConfig-core", "nightConfig-toml"]
|
||||
externalMods-forge-compile = ["moreRed", "iris-forge", "jei-api"]
|
||||
externalMods-forge-runtime = []
|
||||
externalMods-forge-runtime = ["jei-forge"]
|
||||
externalMods-fabric-compile = ["fabricPermissions", "iris-fabric", "jei-api", "rei-api", "rei-builtin"]
|
||||
externalMods-fabric-runtime = []
|
||||
|
||||
|
960
package-lock.json
generated
960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,11 @@
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "^15.2.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-typescript": "^12.0.0",
|
||||
"@rollup/plugin-url": "^8.0.1",
|
||||
"@swc/core": "^1.3.92",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"lightningcss": "^1.22.0",
|
||||
"preact-render-to-string": "^6.2.1",
|
||||
"rehype": "^13.0.0",
|
||||
|
@@ -0,0 +1,166 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.Sheets;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.item.BlockModelWrapper;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.client.resources.model.BlockModelRotation;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.client.resources.model.ResolvedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Vector3f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A standalone model.
|
||||
* <p>
|
||||
* This is very similar to vanilla's {@link BlockModelWrapper}, but suitable for use in both {@link ItemModel}s and
|
||||
* block models. This is primarily intended for use with {@link TurtleUpgradeModel}s.
|
||||
*/
|
||||
public final class StandaloneModel {
|
||||
private final List<BakedQuad> quads;
|
||||
private final boolean useBlockLight;
|
||||
private final TextureAtlasSprite particleIcon;
|
||||
private final RenderType renderType;
|
||||
private final Supplier<Vector3f[]> extents;
|
||||
|
||||
/**
|
||||
* Construct a new {@link StandaloneModel}.
|
||||
*
|
||||
* @param quads The list of quads which form this model.
|
||||
* @param usesBlockLight Whether this uses block lighting. See {@link ItemStackRenderState.LayerRenderState#setUsesBlockLight(boolean)}.
|
||||
* @param particleIcon The sprite for the model's particles. See {@link ItemStackRenderState.LayerRenderState#setParticleIcon(TextureAtlasSprite)}.
|
||||
* @param renderType The render type for this model.
|
||||
*/
|
||||
public StandaloneModel(List<BakedQuad> quads, boolean usesBlockLight, TextureAtlasSprite particleIcon, RenderType renderType) {
|
||||
this.quads = quads;
|
||||
this.useBlockLight = usesBlockLight;
|
||||
this.particleIcon = particleIcon;
|
||||
this.renderType = renderType;
|
||||
this.extents = Suppliers.memoize(() -> BlockModelWrapper.computeExtents(quads));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a model from a {@link ModelBaker} and bake it.
|
||||
*
|
||||
* @param model The model id to load.
|
||||
* @param baker The model baker.
|
||||
* @return The baked {@link StandaloneModel}.
|
||||
*/
|
||||
public static StandaloneModel of(ResourceLocation model, ModelBaker baker) {
|
||||
return of(baker.getModel(model), baker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bake a {@link ResolvedModel} into a {@link StandaloneModel}.
|
||||
*
|
||||
* @param model The resolved model.
|
||||
* @param baker The model baker.
|
||||
* @return The baked {@link StandaloneModel}.
|
||||
*/
|
||||
public static StandaloneModel of(ResolvedModel model, ModelBaker baker) {
|
||||
return baker.compute(new CacheKey(model));
|
||||
}
|
||||
|
||||
private record CacheKey(ResolvedModel model) implements ModelBaker.SharedOperationKey<StandaloneModel> {
|
||||
@Override
|
||||
public StandaloneModel compute(ModelBaker baker) {
|
||||
return ofUncached(model(), baker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof CacheKey(var otherModel) && model() == otherModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return System.identityHashCode(model());
|
||||
}
|
||||
}
|
||||
|
||||
private static StandaloneModel ofUncached(ResolvedModel model, ModelBaker baker) {
|
||||
var slots = model.getTopTextureSlots();
|
||||
return new StandaloneModel(
|
||||
model.bakeTopGeometry(slots, baker, BlockModelRotation.X0_Y0).getAll(),
|
||||
model.getTopGuiLight().lightLikeBlock(),
|
||||
model.resolveParticleSprite(slots, baker),
|
||||
Sheets.translucentItemSheet()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an {@link ItemStackRenderState.LayerRenderState} to render this model.
|
||||
*
|
||||
* @param layer The layer to set up.
|
||||
* @see ItemModel#update(ItemStackRenderState, ItemStack, ItemModelResolver, ItemDisplayContext, ClientLevel, LivingEntity, int)
|
||||
*/
|
||||
public void setupItemLayer(ItemStackRenderState.LayerRenderState layer) {
|
||||
layer.setExtents(extents);
|
||||
layer.setRenderType(renderType);
|
||||
layer.setUsesBlockLight(useBlockLight);
|
||||
layer.setParticleIcon(particleIcon);
|
||||
layer.prepareQuadList().addAll(quads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the model directly.
|
||||
*
|
||||
* @param transform The current pose stack transformations.
|
||||
* @param buffers The buffer source to use for rendering.
|
||||
* @param light The current light texture coordinate.
|
||||
* @param overlay The current overlay texture coordinate.
|
||||
*/
|
||||
public void render(PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
|
||||
render(transform, buffers, light, overlay, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the model directly.
|
||||
*
|
||||
* @param transform The current pose stack transformations.
|
||||
* @param buffers The buffer source to use for rendering.
|
||||
* @param light The current light texture coordinate.
|
||||
* @param overlay The current overlay texture coordinate.
|
||||
* @param tints The tints for this model.
|
||||
*/
|
||||
public void render(PoseStack transform, MultiBufferSource buffers, int light, int overlay, int @Nullable [] tints) {
|
||||
var pose = transform.last();
|
||||
var buffer = buffers.getBuffer(renderType);
|
||||
for (var quad : quads) {
|
||||
float r, g, b, a;
|
||||
var idx = quad.tintIndex();
|
||||
if (tints != null && idx >= 0 && idx < tints.length) {
|
||||
var tint = tints[idx];
|
||||
r = ARGB.red(tint) / 255.0f;
|
||||
g = ARGB.green(tint) / 255.0f;
|
||||
b = ARGB.blue(tint) / 255.0f;
|
||||
a = ARGB.alpha(tint) / 255.0f;
|
||||
} else {
|
||||
r = g = b = a = 1.0f;
|
||||
}
|
||||
|
||||
buffer.putBulkData(pose, quad, r, g, b, a, light, overlay);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
|
||||
package dan200.computercraft.api.client;
|
||||
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* A model to render, combined with a transformation matrix to apply.
|
||||
*/
|
||||
public sealed interface TransformedModel permits TransformedModel.Baked, TransformedModel.Item {
|
||||
record Baked(BakedModel model) implements TransformedModel {
|
||||
}
|
||||
|
||||
record Item(ItemStack stack, Transformation transformation) implements TransformedModel {
|
||||
}
|
||||
|
||||
static TransformedModel of(BakedModel model) {
|
||||
return new TransformedModel.Baked(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
|
||||
*
|
||||
* @param location The location of the model to load.
|
||||
* @return The new {@link TransformedModel} instance.
|
||||
*/
|
||||
static TransformedModel of(ResourceLocation location) {
|
||||
var modelManager = Minecraft.getInstance().getModelManager();
|
||||
return of(ClientPlatformHelper.get().getModel(modelManager, location));
|
||||
}
|
||||
|
||||
static TransformedModel of(ItemStack item, Transformation transform) {
|
||||
return new TransformedModel.Item(item, transform);
|
||||
}
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.StandaloneModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransform;
|
||||
import net.minecraft.client.renderer.item.BlockModelWrapper;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/**
|
||||
* A {@link TurtleUpgradeModel} that renders a basic model.
|
||||
* <p>
|
||||
* This is the {@link TurtleUpgradeModel} equivalent of {@link BlockModelWrapper}.
|
||||
*/
|
||||
public final class BasicUpgradeModel implements TurtleUpgradeModel {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sided");
|
||||
public static final MapCodec<? extends TurtleUpgradeModel.Unbaked> CODEC = RecordCodecBuilder.<Unbaked>mapCodec(instance -> instance.group(
|
||||
ResourceLocation.CODEC.fieldOf("left").forGetter(Unbaked::left),
|
||||
ResourceLocation.CODEC.fieldOf("right").forGetter(Unbaked::right)
|
||||
).apply(instance, Unbaked::new));
|
||||
|
||||
private final StandaloneModel left;
|
||||
private final StandaloneModel right;
|
||||
|
||||
private BasicUpgradeModel(StandaloneModel left, StandaloneModel right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an unbaked {@link BasicUpgradeModel}.
|
||||
*
|
||||
* @param left The model when equipped on the left.
|
||||
* @param right The model when equipped on the right.
|
||||
* @return The unbaked turtle upgrade model.
|
||||
*/
|
||||
public static TurtleUpgradeModel.Unbaked unbaked(ResourceLocation left, ResourceLocation right) {
|
||||
return new Unbaked(left, right);
|
||||
}
|
||||
|
||||
private StandaloneModel getModel(TurtleSide side) {
|
||||
return switch (side) {
|
||||
case LEFT -> left;
|
||||
case RIGHT -> right;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
|
||||
renderer.appendModelIdentityElement(this);
|
||||
renderer.appendModelIdentityElement(side);
|
||||
renderer.appendModelIdentityElement(transform);
|
||||
|
||||
var layer = renderer.newLayer();
|
||||
layer.setTransform(transform);
|
||||
getModel(side).setupItemLayer(layer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
|
||||
getModel(side).render(transform, buffers, light, overlay);
|
||||
}
|
||||
|
||||
private record Unbaked(ResourceLocation left, ResourceLocation right) implements TurtleUpgradeModel.Unbaked {
|
||||
@Override
|
||||
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TurtleUpgradeModel bake(ModelBaker baker) {
|
||||
return new BasicUpgradeModel(StandaloneModel.of(left(), baker), StandaloneModel.of(right(), baker));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
resolver.markDependency(left());
|
||||
resolver.markDependency(right());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,143 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import com.mojang.math.Transformation;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransform;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.renderer.item.TrackingItemStackRenderState;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A sic {@link TurtleUpgradeModel} that renders the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)
|
||||
* upgrade item}.
|
||||
* <p>
|
||||
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
|
||||
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
|
||||
*/
|
||||
public final class ItemUpgradeModel implements TurtleUpgradeModel {
|
||||
private static final TurtleUpgradeModel.Unbaked UNBAKED = new Unbaked();
|
||||
private static final TurtleUpgradeModel INSTANCE = new ItemUpgradeModel();
|
||||
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item");
|
||||
public static final MapCodec<TurtleUpgradeModel.Unbaked> CODEC = MapCodec.unit(UNBAKED);
|
||||
|
||||
private static final TransformedRenderer LEFT = computeRenderer(TurtleSide.LEFT);
|
||||
private static final TransformedRenderer RIGHT = computeRenderer(TurtleSide.RIGHT);
|
||||
|
||||
private ItemUpgradeModel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unbaked {@link ItemUpgradeModel}.
|
||||
*
|
||||
* @return The unbaked item upgrade model.
|
||||
*/
|
||||
public static TurtleUpgradeModel.Unbaked unbaked() {
|
||||
return UNBAKED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
|
||||
renderer.appendModelIdentityElement(this);
|
||||
|
||||
var childState = new TrackingItemStackRenderState();
|
||||
resolver.updateForTopItem(childState, upgrade.getUpgradeItem(), ItemDisplayContext.NONE, null, null, seed);
|
||||
if (!childState.isEmpty()) {
|
||||
renderer.appendModelIdentityElement(childState.getModelIdentity());
|
||||
renderer.appendModelIdentityElement(transform);
|
||||
|
||||
var layer = renderer.newLayer();
|
||||
layer.setTransform(transform);
|
||||
layer.setupSpecialModel(getRenderer(side), childState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
|
||||
transform.mulPose(getRenderer(side).transform().getMatrix());
|
||||
transform.mulPose(Axis.YP.rotation(Mth.PI));
|
||||
Minecraft.getInstance().getItemRenderer().renderStatic(
|
||||
upgrade.getUpgradeItem(), ItemDisplayContext.FIXED, light, overlay, transform, buffers, turtle.getLevel(), 0
|
||||
);
|
||||
}
|
||||
|
||||
private static final class Unbaked implements TurtleUpgradeModel.Unbaked {
|
||||
@Override
|
||||
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TurtleUpgradeModel bake(ModelBaker baker) {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
}
|
||||
}
|
||||
|
||||
private static TransformedRenderer computeRenderer(TurtleSide side) {
|
||||
var pose = new Matrix4f();
|
||||
pose.translate(0.5f, 0.5f, 0.5f);
|
||||
pose.rotate(Axis.YN.rotationDegrees(90f));
|
||||
pose.rotate(Axis.ZP.rotationDegrees(90f));
|
||||
pose.translate(0.0f, 0.0f, side == TurtleSide.RIGHT ? -0.4065f : 0.4065f);
|
||||
return new TransformedRenderer(new Transformation(pose));
|
||||
}
|
||||
|
||||
private static TransformedRenderer getRenderer(TurtleSide side) {
|
||||
return switch (side) {
|
||||
case LEFT -> LEFT;
|
||||
case RIGHT -> RIGHT;
|
||||
};
|
||||
}
|
||||
|
||||
private record TransformedRenderer(Transformation transform) implements SpecialModelRenderer<ItemStackRenderState> {
|
||||
@Override
|
||||
public void render(
|
||||
@Nullable ItemStackRenderState state, ItemDisplayContext itemDisplayContext, PoseStack poseStack,
|
||||
MultiBufferSource multiBufferSource, int overlay, int light, boolean bl
|
||||
) {
|
||||
if (state == null) return;
|
||||
poseStack.pushPose();
|
||||
poseStack.mulPose(transform.getMatrix());
|
||||
state.render(poseStack, multiBufferSource, overlay, light);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getExtents(Set<Vector3f> set) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ItemStackRenderState extractArgument(ItemStack itemStack) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/**
|
||||
* A functional interface to register a {@link TurtleUpgradeModel}.
|
||||
* <p>
|
||||
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
|
||||
* multiple loaders.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RegisterTurtleUpgradeModel {
|
||||
/**
|
||||
* Register a {@link TurtleUpgradeModel}.
|
||||
*
|
||||
* @param id The id used for this type of upgrade model.
|
||||
* @param model The codec used to read/decode an upgrade model.
|
||||
*/
|
||||
void register(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> model);
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.upgrades.UpgradeType;
|
||||
|
||||
/**
|
||||
* A functional interface to register a {@link TurtleUpgradeModeller} for a class of turtle upgrades.
|
||||
* <p>
|
||||
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
|
||||
* multiple loaders.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RegisterTurtleUpgradeModeller {
|
||||
/**
|
||||
* Register a {@link TurtleUpgradeModeller}.
|
||||
*
|
||||
* @param type The turtle upgrade type.
|
||||
* @param modeller The upgrade modeller.
|
||||
* @param <T> The type of the turtle upgrade.
|
||||
*/
|
||||
<T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModeller<T> modeller);
|
||||
}
|
@@ -0,0 +1,209 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.google.common.collect.HashMultiset;
|
||||
import com.google.common.collect.Multiset;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransform;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.renderer.item.SelectItemModel;
|
||||
import net.minecraft.client.resources.model.MissingBlockModel;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link TurtleUpgradeModel} which selects between different models based on the value of a component in
|
||||
* {@linkplain UpgradeData#data() the upgrade's data}.
|
||||
* <p>
|
||||
* This is the {@link TurtleUpgradeModel} equivalent of {@link SelectItemModel}.
|
||||
*
|
||||
* @param <T> The type of value to switch on.
|
||||
*/
|
||||
public final class SelectUpgradeModel<T> implements TurtleUpgradeModel {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "select");
|
||||
public static final MapCodec<? extends TurtleUpgradeModel.Unbaked> CODEC = RecordCodecBuilder.<Unbaked<?>>mapCodec(instance -> instance.group(
|
||||
Cases.CODEC.forGetter(Unbaked::cases),
|
||||
TurtleUpgradeModel.CODEC.optionalFieldOf("fallback").forGetter(Unbaked::fallback)
|
||||
).apply(instance, Unbaked::new));
|
||||
|
||||
private final DataComponentType<T> component;
|
||||
private final Map<T, TurtleUpgradeModel> cases;
|
||||
private final TurtleUpgradeModel fallback;
|
||||
|
||||
private SelectUpgradeModel(DataComponentType<T> component, Map<T, TurtleUpgradeModel> cases, TurtleUpgradeModel fallback) {
|
||||
this.component = component;
|
||||
this.cases = cases;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
private TurtleUpgradeModel getModel(UpgradeData<ITurtleUpgrade> upgrade) {
|
||||
var value = upgrade.get(component);
|
||||
if (value == null) return fallback;
|
||||
|
||||
var model = cases.get(value);
|
||||
return model != null ? model : fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
|
||||
getModel(upgrade).renderForItem(upgrade, side, renderer, resolver, transform, seed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
|
||||
getModel(upgrade).renderForLevel(upgrade, side, turtle, transform, buffers, light, overlay);
|
||||
}
|
||||
|
||||
private record Unbaked<T>(
|
||||
Cases<T> cases,
|
||||
Optional<TurtleUpgradeModel.Unbaked> fallback
|
||||
) implements TurtleUpgradeModel.Unbaked {
|
||||
private static final TurtleUpgradeModel.Unbaked MISSING = BasicUpgradeModel.unbaked(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION);
|
||||
|
||||
@Override
|
||||
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TurtleUpgradeModel bake(ModelBaker baker) {
|
||||
Map<T, TurtleUpgradeModel> cases = new Object2ObjectOpenHashMap<>();
|
||||
for (var condition : cases().cases()) {
|
||||
var model = condition.getSecond().bake(baker);
|
||||
for (var when : condition.getFirst()) cases.put(when, model);
|
||||
}
|
||||
|
||||
return new SelectUpgradeModel<>(cases().component(), cases, fallback().orElse(MISSING).bake(baker));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
cases().cases().forEach(x -> x.getSecond().resolveDependencies(resolver));
|
||||
fallback().orElse(MISSING).resolveDependencies(resolver);
|
||||
}
|
||||
}
|
||||
|
||||
private record Cases<T>(DataComponentType<T> component, List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases) {
|
||||
private static final MapCodec<Cases<?>> CODEC = DataComponentType.CODEC.dispatchMap("property", Cases::component, Util.memoize(Cases::codec));
|
||||
|
||||
private static <T> MapCodec<Cases<T>> codec(DataComponentType<T> component) {
|
||||
return RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
MapCodec.unit(component).forGetter(Cases::component),
|
||||
caseCodec(component.codecOrThrow()).listOf().fieldOf("cases").validate(Cases::validate).forGetter(Cases::cases)
|
||||
).apply(instance, Cases<T>::new));
|
||||
}
|
||||
|
||||
private static <T> Codec<Pair<List<T>, TurtleUpgradeModel.Unbaked>> caseCodec(Codec<T> codec) {
|
||||
return RecordCodecBuilder.create(instance -> instance.group(
|
||||
codec.listOf().fieldOf("when").forGetter(Pair::getFirst),
|
||||
TurtleUpgradeModel.CODEC.fieldOf("model").forGetter(Pair::getSecond)
|
||||
).apply(instance, Pair::new));
|
||||
}
|
||||
|
||||
private static <T> DataResult<List<Pair<List<T>, TurtleUpgradeModel.Unbaked>>> validate(List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases) {
|
||||
Multiset<T> multiset = HashMultiset.create();
|
||||
for (var condition : cases) multiset.addAll(condition.getFirst());
|
||||
|
||||
if (multiset.isEmpty()) return DataResult.error(() -> "Empty cases");
|
||||
if (multiset.size() != multiset.entrySet().size()) {
|
||||
return DataResult.error(() -> "Duplicate case conditions: " + multiset.entrySet().stream()
|
||||
.filter(x -> x.getCount() > 1)
|
||||
.map(x -> Objects.toString(x.getElement()))
|
||||
.collect(Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
return DataResult.success(cases);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link SelectUpgradeModel} that selects a model based on a component.
|
||||
*
|
||||
* @param component The component to select.
|
||||
* @param <T> The type the component stores.
|
||||
* @return A {@link Builder}.
|
||||
*/
|
||||
public static <T> Builder<T> onComponent(DataComponentType<T> component) {
|
||||
return new Builder<>(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for constructing {@link SelectUpgradeModel}s.
|
||||
*
|
||||
* @param <T> The type of value to switch on.
|
||||
*/
|
||||
public static final class Builder<T> {
|
||||
private final DataComponentType<T> component;
|
||||
private final List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases = new ArrayList<>();
|
||||
private TurtleUpgradeModel.@Nullable Unbaked fallback;
|
||||
|
||||
private Builder(DataComponentType<T> component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a case to our model.
|
||||
*
|
||||
* @param value The value for this case.
|
||||
* @param model The model to use.
|
||||
* @return {@code this}, for chaining.
|
||||
*/
|
||||
public Builder<T> when(T value, TurtleUpgradeModel.Unbaked model) {
|
||||
return when(List.of(value), model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a case to our model.
|
||||
*
|
||||
* @param values The value(s) for this case.
|
||||
* @param model The model to use.
|
||||
* @return {@code this}, for chaining.
|
||||
*/
|
||||
public Builder<T> when(List<T> values, TurtleUpgradeModel.Unbaked model) {
|
||||
cases.add(Pair.of(values, model));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a fallback value, when no previous value matches or the component is not present.
|
||||
*
|
||||
* @param model The fallback model.
|
||||
* @return {@code this}, for chaining.
|
||||
*/
|
||||
public Builder<T> fallback(TurtleUpgradeModel.Unbaked model) {
|
||||
this.fallback = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this builder into an unbaked model.
|
||||
*
|
||||
* @return The unbaked {@link SelectUpgradeModel}.
|
||||
*/
|
||||
public TurtleUpgradeModel.Unbaked create() {
|
||||
return new Unbaked<>(new Cases<>(component, cases), Optional.ofNullable(fallback));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.impl.client.ComputerCraftAPIClientService;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransform;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.client.resources.model.ResolvableModel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* The model for a {@link ITurtleUpgrade}.
|
||||
* <p>
|
||||
* Turtle upgrade models are very similar to vanilla's {@link ItemModel}. Each upgrade's model is defined in JSON, and
|
||||
* loaded from resource packs with other assets.
|
||||
* <p>
|
||||
* In most cases, upgrades can use one of the existing implementations of {@link TurtleUpgradeModel} (e.g.
|
||||
* {@link BasicUpgradeModel} or {@link ItemUpgradeModel}), and do not need to subclass it. However, in the cases where
|
||||
* a custom model is required, one should use
|
||||
* {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModel} to register a
|
||||
* model on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent} to register one
|
||||
* on Forge.
|
||||
* <p>
|
||||
* See {@link ITurtleUpgrade} for a full example of registering turtle upgrades and their models.
|
||||
*
|
||||
* @see RegisterTurtleUpgradeModel For multi-loader registration support.
|
||||
* @see ItemUpgradeModel A {@code TurtleUpgradeModel} which uses the upgrade's item.
|
||||
* @see BasicUpgradeModel A {@code TurtleUpgradeModel} which renders a simple model.
|
||||
*/
|
||||
public interface TurtleUpgradeModel {
|
||||
/**
|
||||
* The directory from which turtle upgrade models are loaded. This may be used by data generators.
|
||||
*/
|
||||
String SOURCE = ComputerCraftAPI.MOD_ID + "/turtle_upgrade";
|
||||
|
||||
/**
|
||||
* The codec used to read/write {@linkplain TurtleUpgradeModel.Unbaked unbaked upgrade models}.
|
||||
*/
|
||||
Codec<TurtleUpgradeModel.Unbaked> CODEC = Codec.lazyInitialized(() -> ComputerCraftAPIClientService.get().getTurtleUpgradeModelCodec());
|
||||
|
||||
/**
|
||||
* Render this upgrade to an {@link ItemStackRenderState}. This is used for rendering the item form of the upgrade.
|
||||
* <p>
|
||||
* Like with {@link ItemModel}, implementations must be careful to call {@link ItemStackRenderState#appendModelIdentityElement}
|
||||
* where appropriate.
|
||||
*
|
||||
* @param upgrade The upgrade being rendered.
|
||||
* @param side Which side of the turtle (left or right) the upgrade is equipped on.
|
||||
* @param renderer The render state to draw to.
|
||||
* @param resolver The model resolver.
|
||||
* @param transform The root model's transformation.
|
||||
* @param seed The current model seed.
|
||||
* @see ItemModel#update(ItemStackRenderState, ItemStack, ItemModelResolver, ItemDisplayContext, ClientLevel, LivingEntity, int)
|
||||
*/
|
||||
void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed);
|
||||
|
||||
/**
|
||||
* Render this upgrade to a {@link MultiBufferSource}. This is used for rendering the block-entity form of the
|
||||
* upgrade.
|
||||
*
|
||||
* @param upgrade The upgrade being rendered.
|
||||
* @param side Which side of the turtle (left or right) the upgrade is equipped on.
|
||||
* @param turtle Access to the turtle that the upgrade resides on.
|
||||
* @param transform The current pose stack.
|
||||
* @param buffers The buffers to render to.
|
||||
* @param light The lightmap coordinate.
|
||||
* @param overlay The overlay coordinate.
|
||||
*/
|
||||
void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay);
|
||||
|
||||
/**
|
||||
* An unbaked turtle model. Much like other unbaked models (e.g. {@link ItemModel.Unbaked}), this should resolve
|
||||
* any dependencies and returned the fully-resolved model.
|
||||
*/
|
||||
interface Unbaked extends ResolvableModel {
|
||||
/**
|
||||
* The {@link MapCodec} used to read/write this unbaked model.
|
||||
*
|
||||
* @return The codec used to read/write this model.
|
||||
* @see ItemModel.Unbaked#type()
|
||||
*/
|
||||
MapCodec<? extends Unbaked> type();
|
||||
|
||||
/**
|
||||
* Bake this turtle model.
|
||||
*
|
||||
* @param baker The current model baker
|
||||
* @return The baked upgrade model.
|
||||
* @see ItemModel.Unbaked#bake(ItemModel.BakingContext)
|
||||
*/
|
||||
TurtleUpgradeModel bake(ModelBaker baker);
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import net.minecraft.client.resources.model.UnbakedModel;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Provides models for a {@link ITurtleUpgrade}.
|
||||
* <p>
|
||||
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
|
||||
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
|
||||
* on Forge.
|
||||
*
|
||||
* <h2>Example</h2>
|
||||
* <h3>Fabric</h3>
|
||||
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
|
||||
*
|
||||
* <h3>Forge</h3>
|
||||
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
|
||||
*
|
||||
* @param <T> The type of turtle upgrade this modeller applies to.
|
||||
* @see RegisterTurtleUpgradeModeller For multi-loader registration support.
|
||||
*/
|
||||
public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
|
||||
/**
|
||||
* Obtain the model to be used when rendering a turtle peripheral.
|
||||
* <p>
|
||||
* When the current turtle is {@literal null}, this function should be constant for a given upgrade, side and data.
|
||||
*
|
||||
* @param upgrade The upgrade that you're getting the model for.
|
||||
* @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models.
|
||||
* @param side Which side of the turtle (left or right) the upgrade resides on.
|
||||
* @param data Upgrade data instance for current turtle side.
|
||||
* @return The model that you wish to be used to render your upgrade.
|
||||
*/
|
||||
TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data);
|
||||
|
||||
/**
|
||||
* Get the models that this turtle modeller depends on.
|
||||
* <p>
|
||||
* Models included in this stream will be loaded and baked alongside item and block models, and so may be referenced
|
||||
* by {@link TransformedModel#of(ResourceLocation)}. You do not need to override this method if you will load models
|
||||
* by other means.
|
||||
*
|
||||
* @return A list of models that this modeller depends on.
|
||||
* @see UnbakedModel#resolveDependencies(UnbakedModel.Resolver)
|
||||
*/
|
||||
default Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)}
|
||||
* upgrade item}.
|
||||
* <p>
|
||||
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
|
||||
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
|
||||
*
|
||||
* @param <T> The type of the turtle upgrade.
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> flatItem() {
|
||||
return (TurtleUpgradeModeller<T>) TurtleUpgradeModellers.UPGRADE_ITEM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link TurtleUpgradeModeller} which has a single model for the left and right side.
|
||||
*
|
||||
* @param left The model to use on the left.
|
||||
* @param right The model to use on the right.
|
||||
* @param <T> The type of the turtle upgrade.
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ResourceLocation left, ResourceLocation right) {
|
||||
return new TurtleUpgradeModeller<>() {
|
||||
@Override
|
||||
public TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
|
||||
return TransformedModel.of(side == TurtleSide.LEFT ? left : right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of(left, right);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.math.Axis;
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import org.joml.Matrix4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
final class TurtleUpgradeModellers {
|
||||
private static final Transformation leftTransform = getMatrixFor(TurtleSide.LEFT);
|
||||
private static final Transformation rightTransform = getMatrixFor(TurtleSide.RIGHT);
|
||||
|
||||
private static Transformation getMatrixFor(TurtleSide side) {
|
||||
var pose = new Matrix4f();
|
||||
pose.translate(0.5f, 0.5f, 0.5f);
|
||||
pose.rotate(Axis.YN.rotationDegrees(90f));
|
||||
pose.rotate(Axis.ZP.rotationDegrees(90f));
|
||||
pose.translate(0.0f, 0.0f, side == TurtleSide.RIGHT ? -0.4065f : 0.4065f);
|
||||
return new Transformation(pose);
|
||||
}
|
||||
|
||||
static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
|
||||
|
||||
private static final class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
|
||||
@Override
|
||||
public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
|
||||
return TransformedModel.of(upgrade.getUpgradeItem(data), side == TurtleSide.LEFT ? leftTransform : rightTransform);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.client;
|
||||
|
||||
import dan200.computercraft.impl.Services;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public interface ClientPlatformHelper {
|
||||
/**
|
||||
* Get a model from a resource.
|
||||
*
|
||||
* @param manager The model manager.
|
||||
* @param resourceLocation The model resourceLocation.
|
||||
* @return The baked model.
|
||||
*/
|
||||
BakedModel getModel(ModelManager manager, ResourceLocation resourceLocation);
|
||||
|
||||
static ClientPlatformHelper get() {
|
||||
var instance = Instance.INSTANCE;
|
||||
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
|
||||
}
|
||||
|
||||
final class Instance {
|
||||
static final @Nullable ClientPlatformHelper INSTANCE;
|
||||
static final @Nullable Throwable ERROR;
|
||||
|
||||
static {
|
||||
var helper = Services.tryLoad(ClientPlatformHelper.class);
|
||||
INSTANCE = helper.instance();
|
||||
ERROR = helper.error();
|
||||
}
|
||||
|
||||
private Instance() {
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.client;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
|
||||
import dan200.computercraft.impl.Services;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Bridge between implementation
|
||||
* <p>
|
||||
* Do <strong>NOT</strong> directly reference this class. It exists for internal use by the API.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public interface ComputerCraftAPIClientService {
|
||||
static ComputerCraftAPIClientService get() {
|
||||
var instance = Instance.INSTANCE;
|
||||
return instance == null ? Services.raise(ComputerCraftAPIClientService.class, Instance.ERROR) : instance;
|
||||
}
|
||||
|
||||
Codec<TurtleUpgradeModel.Unbaked> getTurtleUpgradeModelCodec();
|
||||
|
||||
final class Instance {
|
||||
static final @Nullable ComputerCraftAPIClientService INSTANCE;
|
||||
static final @Nullable Throwable ERROR;
|
||||
|
||||
static {
|
||||
var helper = Services.tryLoad(ComputerCraftAPIClientService.class);
|
||||
INSTANCE = helper.instance();
|
||||
ERROR = helper.error();
|
||||
}
|
||||
|
||||
private Instance() {
|
||||
}
|
||||
}
|
||||
}
|
@@ -28,6 +28,20 @@ public class ComputerCraftTags {
|
||||
public static final TagKey<Item> WIRED_MODEM = make("wired_modem");
|
||||
public static final TagKey<Item> MONITOR = make("monitor");
|
||||
|
||||
/**
|
||||
* Floppy disks. Both the read/write version, and treasure disks.
|
||||
*
|
||||
* @since 1.116.0
|
||||
*/
|
||||
public static final TagKey<Item> DISKS = make("disks");
|
||||
|
||||
/**
|
||||
* All pocket computers.
|
||||
*
|
||||
* @since 1.116.0
|
||||
*/
|
||||
public static final TagKey<Item> POCKET_COMPUTERS = make("pocket_computers");
|
||||
|
||||
/**
|
||||
* Items which can be {@linkplain Item#use(Level, Player, InteractionHand) used} when calling
|
||||
* {@code turtle.place()}.
|
||||
|
@@ -6,6 +6,7 @@ package dan200.computercraft.api.component;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.pocket.IPocketAccess;
|
||||
import dan200.computercraft.api.pocket.PocketComputer;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
|
||||
/**
|
||||
@@ -20,7 +21,7 @@ public class ComputerComponents {
|
||||
/**
|
||||
* The {@link IPocketAccess} associated with a pocket computer.
|
||||
*/
|
||||
public static final ComputerComponent<IPocketAccess> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
|
||||
public static final ComputerComponent<PocketComputer> POCKET = ComputerComponent.create(ComputerCraftAPI.MOD_ID, "pocket");
|
||||
|
||||
/**
|
||||
* This component is only present on "command computers", and other computers with admin capabilities.
|
||||
|
@@ -51,10 +51,10 @@ public abstract class ComponentDetailProvider<T> implements DetailProvider<DataC
|
||||
* This method is always called on the server thread, so it is safe to interact with the world here, but you should
|
||||
* take care to avoid long blocking operations as this will stall the server and other computers.
|
||||
*
|
||||
* @param data The full details to be returned for this item stack. New properties should be added to this map.
|
||||
* @param item The component to provide details for.
|
||||
* @param data The full details to be returned for this item stack. New properties should be added to this map.
|
||||
* @param component The component to provide details for.
|
||||
*/
|
||||
public abstract void provideComponentDetails(Map<? super String, Object> data, T item);
|
||||
public abstract void provideComponentDetails(Map<? super String, Object> data, T component);
|
||||
|
||||
@Override
|
||||
public final void provideDetails(Map<? super String, Object> data, DataComponentHolder holder) {
|
||||
|
@@ -7,60 +7,15 @@ package dan200.computercraft.api.pocket;
|
||||
import dan200.computercraft.api.upgrades.UpgradeBase;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Wrapper class for pocket computers.
|
||||
* Access to a pocket computer for {@linkplain IPocketUpgrade pocket upgrades}.
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface IPocketAccess {
|
||||
/**
|
||||
* Get the level in which the pocket computer exists.
|
||||
*
|
||||
* @return The pocket computer's level.
|
||||
*/
|
||||
ServerLevel getLevel();
|
||||
|
||||
/**
|
||||
* Get the position of the pocket computer.
|
||||
*
|
||||
* @return The pocket computer's position.
|
||||
*/
|
||||
Vec3 getPosition();
|
||||
|
||||
/**
|
||||
* Gets the entity holding this item.
|
||||
* <p>
|
||||
* This must be called on the server thread.
|
||||
*
|
||||
* @return The holding entity, or {@code null} if none exists.
|
||||
*/
|
||||
@Nullable
|
||||
Entity getEntity();
|
||||
|
||||
/**
|
||||
* Get the colour of this pocket computer as a RGB number.
|
||||
*
|
||||
* @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and
|
||||
* {@code 0xFFFFFF} or -1 if it has no colour.
|
||||
* @see #setColour(int)
|
||||
*/
|
||||
int getColour();
|
||||
|
||||
/**
|
||||
* Set the colour of the pocket computer to a RGB number.
|
||||
*
|
||||
* @param colour The colour this pocket computer should be changed to. This should be a RGB colour between
|
||||
* {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour.
|
||||
* @see #getColour()
|
||||
*/
|
||||
void setColour(int colour);
|
||||
|
||||
public interface IPocketAccess extends PocketComputer {
|
||||
/**
|
||||
* Get the colour of this pocket computer's light as a RGB number.
|
||||
*
|
||||
@@ -92,7 +47,8 @@ public interface IPocketAccess {
|
||||
/**
|
||||
* Set the upgrade for this pocket computer, also updating the item stack.
|
||||
* <p>
|
||||
* Note this method is not thread safe - it must be called from the server thread.
|
||||
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
|
||||
* active}.
|
||||
*
|
||||
* @param upgrade The new upgrade to set it to, may be {@code null}.
|
||||
* @see #getUpgrade()
|
||||
@@ -114,6 +70,9 @@ public interface IPocketAccess {
|
||||
|
||||
/**
|
||||
* Update the upgrade-specific data.
|
||||
* <p>
|
||||
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
|
||||
* active}.
|
||||
*
|
||||
* @param data The new upgrade data.
|
||||
* @see #getUpgradeData()
|
||||
|
@@ -12,7 +12,7 @@ import dan200.computercraft.impl.ComputerCraftAPIService;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,7 @@ public interface IPocketUpgrade extends UpgradeBase {
|
||||
/**
|
||||
* Called when the pocket computer is right clicked.
|
||||
*
|
||||
* @param world The world the computer is in.
|
||||
* @param level The world the computer is in.
|
||||
* @param access The access object for the pocket item stack.
|
||||
* @param peripheral The peripheral for this upgrade.
|
||||
* @return {@code true} to stop the GUI from opening, otherwise false. You should always provide some code path
|
||||
@@ -79,7 +79,7 @@ public interface IPocketUpgrade extends UpgradeBase {
|
||||
* access the GUI.
|
||||
* @see #createPeripheral(IPocketAccess)
|
||||
*/
|
||||
default boolean onRightClick(Level world, IPocketAccess access, @Nullable IPeripheral peripheral) {
|
||||
default boolean onRightClick(ServerLevel level, IPocketAccess access, @Nullable IPeripheral peripheral) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,81 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.pocket;
|
||||
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A pocket computer.
|
||||
*
|
||||
* @see IPocketAccess
|
||||
* @see dan200.computercraft.api.component.ComputerComponents#POCKET
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface PocketComputer {
|
||||
/**
|
||||
* Get the level in which the pocket computer exists.
|
||||
*
|
||||
* @return The pocket computer's level.
|
||||
*/
|
||||
ServerLevel getLevel();
|
||||
|
||||
/**
|
||||
* Get the position of the pocket computer.
|
||||
*
|
||||
* @return The pocket computer's position.
|
||||
*/
|
||||
Vec3 getPosition();
|
||||
|
||||
/**
|
||||
* Gets the entity holding this item.
|
||||
* <p>
|
||||
* This must be called on the server thread.
|
||||
*
|
||||
* @return The holding entity, or {@code null} if none exists.
|
||||
*/
|
||||
@Nullable
|
||||
Entity getEntity();
|
||||
|
||||
/**
|
||||
* Check whether this pocket computer is currently being held by a player, lectern, or other valid entity.
|
||||
* <p>
|
||||
* As pocket computers are backed by item stacks, you must check for validity before updating the computer.
|
||||
* <p>
|
||||
* This must be called on the server thread.
|
||||
*
|
||||
* @return Whether this computer is active.
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
/**
|
||||
* Get the colour of this pocket computer as an RGB number.
|
||||
*
|
||||
* <p>
|
||||
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
|
||||
* active}.
|
||||
*
|
||||
* @return The colour this pocket computer is. This will be a RGB colour between {@code 0x000000} and
|
||||
* {@code 0xFFFFFF} or -1 if it has no colour.
|
||||
* @see #setColour(int)
|
||||
*/
|
||||
int getColour();
|
||||
|
||||
/**
|
||||
* Set the colour of the pocket computer to an RGB number.
|
||||
* <p>
|
||||
* This method can only be called from the main server thread, when this computer is {@linkplain #isActive() is
|
||||
* active}.
|
||||
*
|
||||
* @param colour The colour this pocket computer should be changed to. This should be a RGB colour between
|
||||
* {@code 0x000000} and {@code 0xFFFFFF} or -1 to reset to the default colour.
|
||||
* @see #getColour()
|
||||
*/
|
||||
void setColour(int colour);
|
||||
|
||||
}
|
@@ -51,16 +51,6 @@ import java.util.function.Function;
|
||||
* <h4>Forge</h4>
|
||||
* {@snippet class=com.example.examplemod.ForgeExampleMod region=turtle_upgrades}
|
||||
*
|
||||
* <h3>Rendering the upgrade</h3>
|
||||
* Next, we need to register a model for our upgrade. This is done by registering a
|
||||
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for your upgrade type.
|
||||
*
|
||||
* <h4>Fabric</h4>
|
||||
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
|
||||
*
|
||||
* <h4>Forge</h4>
|
||||
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
|
||||
*
|
||||
* <h3 id="datagen">Registering the upgrade itself</h3>
|
||||
* Upgrades themselves are loaded from datapacks when a level is loaded. In order to register our new upgrade, we must
|
||||
* create a new JSON file at {@code data/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
|
||||
@@ -71,8 +61,15 @@ import java.util.function.Function;
|
||||
* by the type itself. As our upgrade was defined with {@link UpgradeType#simpleWithCustomItem(Function)}, the
|
||||
* {@code "item"} field will construct our upgrade with {@link Items#COMPASS}.
|
||||
* <p>
|
||||
* Rather than manually creating the file, it is recommended to use data-generators to generate this file. First, we
|
||||
* register our new upgrades into a {@linkplain PatchedRegistries patched registry}.
|
||||
* Similarly, {@linkplain dan200.computercraft.api.client.turtle.TurtleUpgradeModel the upgrade's model} is loaded from
|
||||
* a resource pack, and so we must also create a new JSON file at
|
||||
* {@code assets/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
|
||||
*
|
||||
* {@snippet file=assets/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json}
|
||||
*
|
||||
* Rather than manually creating these file, it is recommended to use data-generators to generate this file. First, we
|
||||
* register our new upgrades into a {@linkplain PatchedRegistries patched registry}. Models must similarly be
|
||||
* registered.
|
||||
*
|
||||
* {@snippet class=com.example.examplemod.data.TurtleUpgradeProvider region=body}
|
||||
*
|
||||
|
@@ -7,8 +7,11 @@ package dan200.computercraft.api.upgrades;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.component.DataComponentGetter;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An upgrade (i.e. a {@link ITurtleUpgrade}) and its current upgrade data.
|
||||
@@ -17,7 +20,9 @@ import net.minecraft.world.item.ItemStack;
|
||||
* @param data The upgrade's data.
|
||||
* @param <T> The type of upgrade, either {@link ITurtleUpgrade} or {@link IPocketUpgrade}.
|
||||
*/
|
||||
public record UpgradeData<T extends UpgradeBase>(Holder.Reference<T> holder, DataComponentPatch data) {
|
||||
public record UpgradeData<T extends UpgradeBase>(
|
||||
Holder.Reference<T> holder, DataComponentPatch data
|
||||
) implements DataComponentGetter {
|
||||
/**
|
||||
* A utility method to construct a new {@link UpgradeData} instance.
|
||||
*
|
||||
@@ -66,4 +71,17 @@ public record UpgradeData<T extends UpgradeBase>(Holder.Reference<T> holder, Dat
|
||||
public ItemStack getUpgradeItem() {
|
||||
return upgrade().getUpgradeItem(data).copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a component from the upgrade's {@link #data()} .
|
||||
*
|
||||
* @param component The component get.
|
||||
* @param <U> The type of the component's value.
|
||||
* @return The component, or {@code null} if not present.
|
||||
*/
|
||||
@Override
|
||||
public <U> @Nullable U get(DataComponentType<? extends U> component) {
|
||||
var result = data().get(component);
|
||||
return result == null ? null : result.orElse(null);
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ plugins {
|
||||
sourceSets.client {
|
||||
java {
|
||||
exclude("dan200/computercraft/client/integration/emi")
|
||||
exclude("dan200/computercraft/client/integration/jei")
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -39,7 +39,6 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -137,20 +136,20 @@ public final class ClientHooks {
|
||||
if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.holder().key().location()));
|
||||
}
|
||||
|
||||
public static @Nullable BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
|
||||
public static BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
|
||||
// Only apply to cables which have both a cable and modem
|
||||
if (state.getBlock() != ModRegistry.Blocks.CABLE.get()
|
||||
|| !state.getValue(CableBlock.CABLE)
|
||||
|| state.getValue(CableBlock.MODEM) == CableModemVariant.None
|
||||
) {
|
||||
return null;
|
||||
return state;
|
||||
}
|
||||
|
||||
var hit = Minecraft.getInstance().hitResult;
|
||||
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return null;
|
||||
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return state;
|
||||
var hitPos = ((BlockHitResult) hit).getBlockPos();
|
||||
|
||||
if (!hitPos.equals(pos)) return null;
|
||||
if (!hitPos.equals(pos)) return state;
|
||||
|
||||
return WorldUtil.isVecInside(CableShapes.getModemShape(state), hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()))
|
||||
? state.getBlock().defaultBlockState().setValue(CableBlock.MODEM, state.getValue(CableBlock.MODEM))
|
||||
|
@@ -4,51 +4,53 @@
|
||||
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.client.StandaloneModel;
|
||||
import dan200.computercraft.api.client.turtle.*;
|
||||
import dan200.computercraft.client.gui.*;
|
||||
import dan200.computercraft.client.item.colour.PocketComputerLight;
|
||||
import dan200.computercraft.client.item.model.TurtleOverlayModel;
|
||||
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
|
||||
import dan200.computercraft.client.item.properties.PocketComputerStateProperty;
|
||||
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.platform.ModelKey;
|
||||
import dan200.computercraft.client.render.CustomLecternRenderer;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
|
||||
import dan200.computercraft.client.turtle.TurtleModemModeller;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.CommandComputerCraft;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.color.item.ItemTintSource;
|
||||
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
|
||||
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
|
||||
import net.minecraft.client.gui.screens.MenuScreens;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
|
||||
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.client.resources.model.MissingBlockModel;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ResolvableModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
|
||||
@@ -62,6 +64,19 @@ public final class ClientRegistry {
|
||||
private ClientRegistry() {
|
||||
}
|
||||
|
||||
private static final Map<ResourceLocation, ModelKey<StandaloneModel>> models = new ConcurrentHashMap<>();
|
||||
|
||||
public static ModelKey<StandaloneModel> getModel(ResourceLocation model) {
|
||||
return models.computeIfAbsent(model, m -> ClientPlatformHelper.get().createModelKey(m::toString));
|
||||
}
|
||||
|
||||
public static StandaloneModel getModel(ModelManager manager, ResourceLocation modelId) {
|
||||
var model = getModel(modelId).get(manager);
|
||||
if (model != null) return model;
|
||||
|
||||
return Objects.requireNonNull(getModel(MissingBlockModel.LOCATION).get(manager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any client-side objects which don't have to be done on the main thread.
|
||||
*/
|
||||
@@ -87,21 +102,10 @@ public final class ClientRegistry {
|
||||
<M extends AbstractContainerMenu, U extends Screen & MenuAccess<M>> void register(MenuType<? extends M> type, MenuScreens.ScreenConstructor<M, U> factory);
|
||||
}
|
||||
|
||||
public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.SPEAKER.get(), TurtleUpgradeModeller.sided(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
|
||||
));
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.WORKBENCH.get(), TurtleUpgradeModeller.sided(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
|
||||
));
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.WIRELESS_MODEM.get(), new TurtleModemModeller());
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.TOOL.get(), TurtleUpgradeModeller.flatItem());
|
||||
}
|
||||
|
||||
public static void registerReloadListeners(BiConsumer<ResourceLocation, PreparableReloadListener> register, Minecraft minecraft) {
|
||||
register.accept(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sprites"), GuiSprites.initialise(minecraft.getTextureManager()));
|
||||
public static void registerTurtleModels(RegisterTurtleUpgradeModel register) {
|
||||
register.register(BasicUpgradeModel.ID, BasicUpgradeModel.CODEC);
|
||||
register.register(ItemUpgradeModel.ID, ItemUpgradeModel.CODEC);
|
||||
register.register(SelectUpgradeModel.ID, SelectUpgradeModel.CODEC);
|
||||
}
|
||||
|
||||
private static final ResourceLocation[] EXTRA_MODELS = {
|
||||
@@ -109,17 +113,72 @@ public final class ClientRegistry {
|
||||
TurtleBlockEntityRenderer.NORMAL_TURTLE_MODEL,
|
||||
TurtleBlockEntityRenderer.ADVANCED_TURTLE_MODEL,
|
||||
TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
|
||||
MissingBlockModel.LOCATION,
|
||||
};
|
||||
|
||||
public static void registerExtraModels(Consumer<ResourceLocation> register, Collection<ResourceLocation> extraModels) {
|
||||
for (var model : EXTRA_MODELS) register.accept(model);
|
||||
extraModels.forEach(register);
|
||||
TurtleUpgradeModellers.getDependencies().forEach(register);
|
||||
/**
|
||||
* Additional models to load.
|
||||
*
|
||||
* @param turtleOverlays The unbaked turtle models.
|
||||
* @param turtleUpgrades The unbaked turtle upgrades.
|
||||
* @see #gatherExtraModels(ResourceManager, Executor)
|
||||
* @see #registerExtraModels(RegisterExtraModels, ExtraModels)
|
||||
*/
|
||||
public record ExtraModels(
|
||||
Map<ResourceLocation, TurtleOverlay.Unbaked> turtleOverlays,
|
||||
Map<ResourceLocation, TurtleUpgradeModel.Unbaked> turtleUpgrades
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather the list of extra models to load.
|
||||
*
|
||||
* @param resources The current resource manager.
|
||||
* @param executor The executor to schedule loading on.
|
||||
* @return A promise which contains our extra models.
|
||||
*/
|
||||
public static CompletableFuture<ExtraModels> gatherExtraModels(ResourceManager resources, Executor executor) {
|
||||
var turtleOverlays = TurtleOverlayManager.loader().load(resources, executor);
|
||||
var turtleUpgrades = TurtleUpgradeModelManager.loader().load(resources, executor);
|
||||
return turtleOverlays.thenCombine(turtleUpgrades, ExtraModels::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback used to register a model for a {@link ModelKey}.
|
||||
*/
|
||||
public interface RegisterExtraModels {
|
||||
default <U extends ResolvableModel, T> void register(ModelKey<T> key, U unbaked, BiFunction<U, ModelBaker, T> bake) {
|
||||
register(key, unbaked, ResolvableModel::resolveDependencies, bake);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an extra model.
|
||||
* <p>
|
||||
* This accepts functions to resolve dependencies and bake the model. While this would be conceptually nicer as
|
||||
* an interface, it would require multiple adaptors to convert between "upgrade model", "a"bstract model" and
|
||||
* "platform-specific model", so working with functions is cleaner.
|
||||
*
|
||||
* @param key The model key for this model.
|
||||
* @param unbaked The unbaked model.
|
||||
* @param resolve The function to resolve dependencies for this model.
|
||||
* @param bake The function to bake this model.
|
||||
* @param <U> The type of unbaked model.
|
||||
* @param <T> The type of baked model.
|
||||
*/
|
||||
<U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake);
|
||||
}
|
||||
|
||||
public static void registerExtraModels(RegisterExtraModels register, ExtraModels models) {
|
||||
for (var model : EXTRA_MODELS) {
|
||||
register.register(getModel(model), model, (id, r) -> r.markDependency(id), StandaloneModel::of);
|
||||
}
|
||||
TurtleOverlayManager.loader().register(register, models.turtleOverlays());
|
||||
TurtleUpgradeModelManager.loader().register(register, models.turtleUpgrades());
|
||||
}
|
||||
|
||||
public static void registerItemModels(BiConsumer<ResourceLocation, MapCodec<? extends ItemModel.Unbaked>> register) {
|
||||
register.accept(TurtleOverlayModel.ID, TurtleOverlayModel.CODEC);
|
||||
register.accept(TurtleUpgradeModel.ID, TurtleUpgradeModel.CODEC);
|
||||
register.accept(dan200.computercraft.client.item.model.TurtleUpgradeModel.ID, dan200.computercraft.client.item.model.TurtleUpgradeModel.CODEC);
|
||||
}
|
||||
|
||||
public static void registerItemColours(BiConsumer<ResourceLocation, MapCodec<? extends ItemTintSource>> register) {
|
||||
@@ -134,44 +193,11 @@ public final class ClientRegistry {
|
||||
register.accept(TurtleShowElfOverlay.ID, TurtleShowElfOverlay.CODEC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register client-side commands.
|
||||
*
|
||||
* @param dispatcher The dispatcher to register the commands to.
|
||||
* @param sendError A function to send an error message.
|
||||
* @param <T> The type of the client-side command context.
|
||||
*/
|
||||
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
|
||||
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
|
||||
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
|
||||
.then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
|
||||
.executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
|
||||
));
|
||||
public interface RegisterPictureInPictureRenderer {
|
||||
<T extends PictureInPictureRenderState> void register(Class<T> state, Function<MultiBufferSource.BufferSource, PictureInPictureRenderer<T>> factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
|
||||
*
|
||||
* @param context The command context.
|
||||
* @param sendError A function to send an error message.
|
||||
* @param id The computer's id.
|
||||
* @param <T> The type of the client-side command context.
|
||||
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
|
||||
*/
|
||||
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
|
||||
var server = Minecraft.getInstance().getSingleplayerServer();
|
||||
if (server == null) {
|
||||
sendError.accept(context, Component.literal("Not on a single-player server"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
|
||||
if (!file.isDirectory()) {
|
||||
sendError.accept(context, Component.literal("Computer's folder does not exist"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
Util.getPlatform().openFile(file);
|
||||
return 1;
|
||||
public static void registerPictureInPictureRenderers(RegisterPictureInPictureRenderer register) {
|
||||
register.register(PrintoutScreen.PrintoutRenderState.class, PrintoutScreen.PrintoutPictureRenderer::new);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,95 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.platform.ModelKey;
|
||||
import dan200.computercraft.shared.util.ResourceUtils;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ResolvableModel;
|
||||
import net.minecraft.resources.FileToIdConverter;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* A manager for loading custom models. This is responsible for {@linkplain #load(ResourceManager, Executor) loading
|
||||
* models from resource packs}, {@linkplain #register(ClientRegistry.RegisterExtraModels, Map) registering them as
|
||||
* extra models}, and then {@linkplain #get(ModelManager, ResourceLocation) looking them up}.
|
||||
*
|
||||
* @param <U> The type of unbaked model.
|
||||
* @param <T> The type of baked model.
|
||||
*/
|
||||
public class CustomModelManager<U extends ResolvableModel, T> {
|
||||
private final String kind;
|
||||
private final FileToIdConverter lister;
|
||||
private final Codec<U> codec;
|
||||
private final BiFunction<U, ModelBaker, T> bake;
|
||||
|
||||
private final ModelKey<T> missingModelKey;
|
||||
private final U missingModel;
|
||||
|
||||
private final Map<ResourceLocation, ModelKey<T>> modelKeys = new ConcurrentHashMap<>();
|
||||
|
||||
public CustomModelManager(String kind, FileToIdConverter lister, Codec<U> codec, BiFunction<U, ModelBaker, T> bake, U missingModel) {
|
||||
this.kind = kind;
|
||||
this.lister = lister;
|
||||
this.codec = codec;
|
||||
this.bake = bake;
|
||||
|
||||
this.missingModelKey = ClientPlatformHelper.get().createModelKey(() -> "Missing " + kind);
|
||||
this.missingModel = missingModel;
|
||||
}
|
||||
|
||||
private ModelKey<T> getModelKey(ResourceLocation id) {
|
||||
return modelKeys.computeIfAbsent(id, o -> ClientPlatformHelper.get().createModelKey(() -> kind + " " + o));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load our models from resources.
|
||||
*
|
||||
* @param resources The current resource manager.
|
||||
* @param executor The executor to schedule work on.
|
||||
* @return The map of unbaked models.
|
||||
*/
|
||||
public CompletableFuture<Map<ResourceLocation, U>> load(ResourceManager resources, Executor executor) {
|
||||
return ResourceUtils.load(resources, executor, kind, lister, JsonOps.INSTANCE, codec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register our unbaked models.
|
||||
*
|
||||
* @param register The callback to register models with.
|
||||
* @param models The models to register.
|
||||
*/
|
||||
public void register(ClientRegistry.RegisterExtraModels register, Map<ResourceLocation, U> models) {
|
||||
models.forEach((id, model) -> register.register(getModelKey(id), model, bake));
|
||||
register.register(missingModelKey, missingModel, bake);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the model with the given id. If the model does not exist, then the missing model is returned instead.
|
||||
*
|
||||
* @param modelManager The model manager.
|
||||
* @param id The model id.
|
||||
* @return The loaded model.
|
||||
*/
|
||||
public T get(ModelManager modelManager, ResourceLocation id) {
|
||||
var model = getModelKey(id).get(modelManager);
|
||||
if (model != null) return model;
|
||||
|
||||
var missing = missingModelKey.get(modelManager);
|
||||
if (missing == null) throw new IllegalStateException("Models have not yet been loaded.");
|
||||
return missing;
|
||||
}
|
||||
}
|
@@ -125,6 +125,15 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|
||||
return super.mouseReleased(x, y, button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
// Reimplement ContainerEventHandler.mouseScrolled, as AbstractContainerScreen overrides it.
|
||||
var child = getChildAt(mouseX, mouseY);
|
||||
if (child.isPresent() && child.get().mouseScrolled(mouseX, mouseY, scrollX, scrollY)) return true;
|
||||
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
|
||||
super.render(graphics, mouseX, mouseY, partialTicks);
|
||||
|
@@ -6,10 +6,10 @@ package dan200.computercraft.client.gui;
|
||||
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
@@ -39,14 +39,16 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra
|
||||
public void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
// Draw a border around the terminal
|
||||
var terminal = getTerminal();
|
||||
var computerTextures = GuiSprites.getComputerTextures(family);
|
||||
|
||||
SpriteRenderer.inGui(graphics, spriteRenderer -> {
|
||||
var computerTextures = GuiSprites.getComputerTextures(family);
|
||||
ComputerBorderRenderer.render(
|
||||
spriteRenderer, computerTextures,
|
||||
terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
|
||||
);
|
||||
ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
|
||||
});
|
||||
graphics.blitSprite(
|
||||
RenderPipelines.GUI_TEXTURED, computerTextures.border(),
|
||||
terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2
|
||||
);
|
||||
|
||||
graphics.blitSprite(
|
||||
RenderPipelines.GUI_TEXTURED, Nullability.assertNonNull(computerTextures.sidebar()),
|
||||
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -24,7 +24,7 @@ public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
|
||||
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -7,9 +7,6 @@ package dan200.computercraft.client.gui;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.client.renderer.texture.TextureManager;
|
||||
import net.minecraft.client.resources.TextureAtlasHolder;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@@ -19,10 +16,7 @@ import java.util.stream.Stream;
|
||||
/**
|
||||
* Sprite sheet for all GUI texutres in the mod.
|
||||
*/
|
||||
public final class GuiSprites extends TextureAtlasHolder {
|
||||
public static final ResourceLocation SPRITE_SHEET = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui");
|
||||
public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png");
|
||||
|
||||
public final class GuiSprites {
|
||||
public static final ButtonTextures TURNED_OFF = button("turned_off");
|
||||
public static final ButtonTextures TURNED_ON = button("turned_on");
|
||||
public static final ButtonTextures TERMINATE = button("terminate");
|
||||
@@ -32,6 +26,9 @@ public final class GuiSprites extends TextureAtlasHolder {
|
||||
public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true);
|
||||
public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false);
|
||||
|
||||
private GuiSprites() {
|
||||
}
|
||||
|
||||
private static ButtonTextures button(String name) {
|
||||
return new ButtonTextures(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "buttons/" + name),
|
||||
@@ -47,34 +44,6 @@ public final class GuiSprites extends TextureAtlasHolder {
|
||||
);
|
||||
}
|
||||
|
||||
private static @Nullable GuiSprites instance;
|
||||
|
||||
private GuiSprites(TextureManager textureManager) {
|
||||
super(textureManager, TEXTURE, SPRITE_SHEET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the singleton {@link GuiSprites} instance.
|
||||
*
|
||||
* @param textureManager The current texture manager.
|
||||
* @return The singleton {@link GuiSprites} instance, to register as resource reload listener.
|
||||
*/
|
||||
public static GuiSprites initialise(TextureManager textureManager) {
|
||||
if (instance != null) throw new IllegalStateException("GuiSprites has already been initialised");
|
||||
return instance = new GuiSprites(textureManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a texture on the atlas.
|
||||
*
|
||||
* @param texture The texture to find.
|
||||
* @return The sprite on the atlas.
|
||||
*/
|
||||
public static TextureAtlasSprite get(ResourceLocation texture) {
|
||||
if (instance == null) throw new IllegalStateException("GuiSprites has not been initialised");
|
||||
return instance.getSprite(texture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate textures to use for a particular computer family.
|
||||
*
|
||||
|
@@ -9,7 +9,7 @@ import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.toasts.Toast;
|
||||
import net.minecraft.client.gui.components.toasts.ToastManager;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
@@ -91,7 +91,7 @@ public class ItemToast implements Toast {
|
||||
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, Font font, long time) {
|
||||
graphics.blitSprite(RenderType::guiTextured, TEXTURE, 0, 0, width(), height());
|
||||
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, width(), height());
|
||||
|
||||
var textX = MARGIN;
|
||||
if (!stack.isEmpty()) {
|
||||
|
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
/**
|
||||
* Supports for converting/translating key codes.
|
||||
*/
|
||||
public class KeyConverter {
|
||||
/**
|
||||
* GLFW's key events refer to the physical key code, rather than the "actual" key code (with keyboard layout
|
||||
* applied).
|
||||
* <p>
|
||||
* This makes sense for WASD-style input, but is a right pain for keyboard shortcuts — this function attempts to
|
||||
* translate those keys back to their "actual" key code. See also
|
||||
* <a href="https://github.com/glfw/glfw/issues/1502"> this discussion on GLFW's GitHub.</a>
|
||||
*
|
||||
* @param key The current key code.
|
||||
* @param scanCode The current scan code.
|
||||
* @return The translated key code.
|
||||
*/
|
||||
public static int physicalToActual(int key, int scanCode) {
|
||||
var name = GLFW.glfwGetKeyName(key, scanCode);
|
||||
if (name == null || name.length() != 1) return key;
|
||||
|
||||
// If we've got a single character as the key name, treat that as the ASCII value of the key,
|
||||
// and map that back to a key code.
|
||||
var character = name.charAt(0);
|
||||
|
||||
// 0-9 and A-Z map directly to their GLFW key (they're the same ASCII code).
|
||||
if ((character >= '0' && character <= '9') || (character >= 'A' && character <= 'Z')) return character;
|
||||
// a-z map to GLFW_KEY_{A,Z}
|
||||
if (character >= 'a' && character <= 'z') return GLFW.GLFW_KEY_A + (character - 'a');
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
@@ -72,8 +72,8 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
var direction = scrollHandler.onMouseScroll(scrollX, scrollY);
|
||||
var inventory = Objects.requireNonNull(minecraft().player).getInventory();
|
||||
inventory.setSelectedHotbarSlot(ScrollWheelHandler.getNextScrollWheelSelection(
|
||||
direction.y == 0 ? -direction.x : direction.y, inventory.selected, Inventory.getSelectionSize()
|
||||
inventory.setSelectedSlot(ScrollWheelHandler.getNextScrollWheelSelection(
|
||||
direction.y == 0 ? -direction.x : direction.y, inventory.getSelectedSlot(), Inventory.getSelectionSize()
|
||||
));
|
||||
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
@@ -108,7 +108,7 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
|
||||
var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8));
|
||||
var y = 10;
|
||||
for (var line : lines) {
|
||||
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFF, true);
|
||||
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFFFF, true);
|
||||
y += 9;
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.MultiLineLabel;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -88,13 +88,13 @@ public final class OptionScreen extends Screen {
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
|
||||
// Render the actual texture.
|
||||
graphics.blit(RenderType::guiTextured, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
|
||||
graphics.blit(RenderType::guiTextured, BACKGROUND,
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND,
|
||||
x, y + PADDING, 0, PADDING, innerWidth, innerHeight - PADDING * 2,
|
||||
innerWidth, PADDING,
|
||||
256, 256
|
||||
);
|
||||
graphics.blit(RenderType::guiTextured, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
|
||||
|
||||
assertNonNull(messageRenderer).renderLeftAlignedNoShadow(graphics, x + PADDING, y + PADDING, FONT_HEIGHT, 0x404040);
|
||||
super.render(graphics, mouseX, mouseY, partialTicks);
|
||||
|
@@ -7,7 +7,7 @@ package dan200.computercraft.client.gui;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -24,10 +24,10 @@ public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
|
||||
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
|
||||
if (getMenu().isPrinting()) {
|
||||
graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,18 +4,27 @@
|
||||
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.client.render.PrintoutRenderer;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
|
||||
import net.minecraft.client.gui.render.state.GuiElementRenderState;
|
||||
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerListener;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Matrix3x2f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -25,7 +34,7 @@ import static dan200.computercraft.client.render.PrintoutRenderer.*;
|
||||
/**
|
||||
* The GUI for printed pages and books.
|
||||
*
|
||||
* @see dan200.computercraft.client.render.PrintoutRenderer
|
||||
* @see PrintoutRenderer
|
||||
*/
|
||||
public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu> implements ContainerListener {
|
||||
private PrintoutInfo printout = PrintoutInfo.DEFAULT;
|
||||
@@ -113,15 +122,11 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
// Push the printout slightly forward, to avoid clipping into the background.
|
||||
graphics.pose().pushPose();
|
||||
graphics.pose().translate(0, 0, 1);
|
||||
|
||||
graphics.drawSpecial(bufferSource -> {
|
||||
drawBorder(graphics.pose(), bufferSource, leftPos, topPos, 0, page, printout.pages(), printout.book(), LightTexture.FULL_BRIGHT);
|
||||
drawText(graphics.pose(), bufferSource, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, LightTexture.FULL_BRIGHT, printout.text(), printout.colour());
|
||||
});
|
||||
|
||||
graphics.pose().popPose();
|
||||
graphics.guiRenderState.submitPicturesInPictureState(new PrintoutRenderState(
|
||||
leftPos - COVER_SIZE - 32, leftPos + X_SIZE + COVER_SIZE + 32,
|
||||
topPos - COVER_SIZE, topPos + Y_SIZE + COVER_SIZE,
|
||||
printout, page, new Matrix3x2f(graphics.pose()), graphics.scissorStack.peek()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,4 +151,56 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
|
||||
return new PrintoutInfo(pages, book, text, colours);
|
||||
}
|
||||
}
|
||||
|
||||
public record PrintoutRenderState(
|
||||
int x0, int x1, int y0, int y1, PrintoutInfo printout, int page, Matrix3x2f pose,
|
||||
@Nullable ScreenRectangle scissorArea, @Nullable ScreenRectangle bounds
|
||||
) implements PictureInPictureRenderState {
|
||||
private PrintoutRenderState(
|
||||
int x0, int x1, int y0, int y1, PrintoutInfo printout, int page, Matrix3x2f pose, @Nullable ScreenRectangle scissorArea
|
||||
) {
|
||||
this(x0, x1, y0, y1, printout, page, pose, scissorArea, PictureInPictureRenderState.getBounds(x0, x1, y0, y1, scissorArea));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float scale() {
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PIP renderer for printouts.
|
||||
* <p>
|
||||
* We prefer using a PIP (rather than a {@link GuiElementRenderState}), as {@link PrintoutRenderer} renders with
|
||||
* multiple z-levels.
|
||||
*/
|
||||
public static final class PrintoutPictureRenderer extends PictureInPictureRenderer<PrintoutRenderState> {
|
||||
public PrintoutPictureRenderer(MultiBufferSource.BufferSource bufferSource) {
|
||||
super(bufferSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderToTexture(PrintoutRenderState state, PoseStack pose) {
|
||||
pose.pushPose();
|
||||
pose.translate(-0.5f * X_SIZE, -(Y_SIZE + COVER_SIZE), 0);
|
||||
pose.scale(1.0f, 1.0f, -1.0f);
|
||||
|
||||
drawBorder(pose, bufferSource, 0, 0, 0, state.page(), state.printout().pages(), state.printout().book(), LightTexture.FULL_BRIGHT);
|
||||
drawText(
|
||||
pose, bufferSource, X_TEXT_MARGIN, Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * state.page(), LightTexture.FULL_BRIGHT,
|
||||
state.printout().text(), state.printout().colour()
|
||||
);
|
||||
pose.popPose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<PrintoutRenderState> getRenderStateClass() {
|
||||
return PrintoutRenderState.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTextureLabel() {
|
||||
return "Printout";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,12 +7,12 @@ package dan200.computercraft.client.gui;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -50,7 +50,7 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
var advanced = family == ComputerFamily.ADVANCED;
|
||||
graphics.blit(
|
||||
RenderType::guiTextured, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL,
|
||||
RenderPipelines.GUI_TEXTURED, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL,
|
||||
leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0,
|
||||
TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE
|
||||
);
|
||||
@@ -61,14 +61,15 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
var slotX = slot % 4;
|
||||
var slotY = slot / 4;
|
||||
graphics.blitSprite(
|
||||
RenderType::guiTextured, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
|
||||
RenderPipelines.GUI_TEXTURED, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 22, 22
|
||||
);
|
||||
}
|
||||
|
||||
// Render sidebar
|
||||
SpriteRenderer.inGui(graphics, spriteRenderer ->
|
||||
ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset)
|
||||
graphics.blitSprite(
|
||||
RenderPipelines.GUI_TEXTURED, Nullability.assertNonNull(GuiSprites.getComputerTextures(family).sidebar()),
|
||||
leftPos, topPos + sidebarYOffset, AbstractComputerMenu.SIDEBAR_WIDTH, ComputerSidebar.HEIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,7 @@ package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.shared.computer.core.InputHandler;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
@@ -24,12 +22,9 @@ public final class ComputerSidebar {
|
||||
private static final int ICON_MARGIN = 2;
|
||||
|
||||
private static final int CORNERS_BORDER = 3;
|
||||
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
|
||||
|
||||
private static final int BUTTONS = 2;
|
||||
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
|
||||
|
||||
private static final int TEX_HEIGHT = 14;
|
||||
public static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
|
||||
|
||||
private ComputerSidebar() {
|
||||
}
|
||||
@@ -63,14 +58,6 @@ public final class ComputerSidebar {
|
||||
));
|
||||
}
|
||||
|
||||
public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) {
|
||||
var texture = textures.sidebar();
|
||||
if (texture == null) throw new NullPointerException(textures + " has no sidebar texture");
|
||||
var sprite = GuiSprites.get(texture);
|
||||
|
||||
renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT);
|
||||
}
|
||||
|
||||
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {
|
||||
if (isOn.getAsBoolean()) {
|
||||
input.shutdown();
|
||||
|
@@ -9,7 +9,7 @@ import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -48,7 +48,7 @@ public class DynamicImageButton extends Button {
|
||||
setTooltip(message.tooltip());
|
||||
|
||||
var texture = this.texture.get(isHoveredOrFocused());
|
||||
graphics.blitSprite(RenderType::guiTextured, texture, getX(), getY(), width, height);
|
||||
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, texture, getX(), getY(), width, height);
|
||||
}
|
||||
|
||||
public record HintedMessage(Component message, Tooltip tooltip) {
|
||||
|
@@ -4,6 +4,9 @@
|
||||
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.gui.KeyConverter;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
@@ -13,8 +16,16 @@ import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.narration.NarratedElementType;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||
import net.minecraft.client.gui.render.TextureSetup;
|
||||
import net.minecraft.client.gui.render.state.GuiElementRenderState;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.joml.Matrix3x2f;
|
||||
import org.joml.Matrix4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.BitSet;
|
||||
@@ -82,7 +93,7 @@ public class TerminalWidget extends AbstractWidget {
|
||||
}
|
||||
|
||||
if ((modifiers & GLFW.GLFW_MOD_CONTROL) != 0) {
|
||||
switch (key) {
|
||||
switch (KeyConverter.physicalToActual(key, scancode)) {
|
||||
case GLFW.GLFW_KEY_T -> {
|
||||
if (terminateTimer < 0) terminateTimer = 0;
|
||||
}
|
||||
@@ -118,7 +129,7 @@ public class TerminalWidget extends AbstractWidget {
|
||||
computer.keyUp(key);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
switch (KeyConverter.physicalToActual(key, scancode)) {
|
||||
case GLFW.GLFW_KEY_T -> terminateTimer = -1;
|
||||
case GLFW.GLFW_KEY_R -> rebootTimer = -1;
|
||||
case GLFW.GLFW_KEY_S -> shutdownTimer = -1;
|
||||
@@ -253,12 +264,23 @@ public class TerminalWidget extends AbstractWidget {
|
||||
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
|
||||
if (!visible) return;
|
||||
|
||||
graphics.drawSpecial(bufferSource -> {
|
||||
FixedWidthFontRenderer.drawTerminal(
|
||||
FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
|
||||
(float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
|
||||
);
|
||||
});
|
||||
var scissor = graphics.scissorStack.peek();
|
||||
var terminalPose = new Matrix3x2f(graphics.pose());
|
||||
var terminalTextures = TextureSetup.singleTextureWithLightmap(graphics.minecraft.getTextureManager().getTexture(FixedWidthFontRenderer.FONT).getTextureView());
|
||||
|
||||
graphics.guiRenderState.submitGuiElement(new TerminalBackgroundRenderState(
|
||||
innerX, innerY, terminal, terminalPose, terminalTextures,
|
||||
maybeIntersect(scissor, new ScreenRectangle(
|
||||
innerX, innerY, terminal.getWidth() * FONT_WIDTH, terminal.getHeight() * FONT_HEIGHT).transformMaxBounds(graphics.pose())
|
||||
),
|
||||
scissor
|
||||
));
|
||||
|
||||
graphics.guiRenderState.submitGuiElement(new TerminalTextRenderState(
|
||||
innerX, innerY, terminal, terminalPose, terminalTextures,
|
||||
maybeIntersect(scissor, new ScreenRectangle(getX(), getY(), getWidth(), getHeight()).transformMaxBounds(graphics.pose())),
|
||||
scissor
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -273,4 +295,49 @@ public class TerminalWidget extends AbstractWidget {
|
||||
public static int getHeight(int termHeight) {
|
||||
return termHeight * FONT_HEIGHT + MARGIN * 2;
|
||||
}
|
||||
|
||||
private static @Nullable ScreenRectangle maybeIntersect(@Nullable ScreenRectangle scissor, ScreenRectangle bounds) {
|
||||
return scissor == null ? bounds : bounds.intersection(scissor);
|
||||
}
|
||||
|
||||
private record TerminalBackgroundRenderState(
|
||||
int x, int y, Terminal terminal,
|
||||
Matrix3x2f pose,
|
||||
TextureSetup textureSetup,
|
||||
@Nullable ScreenRectangle bounds,
|
||||
@Nullable ScreenRectangle scissorArea
|
||||
) implements GuiElementRenderState {
|
||||
@Override
|
||||
public void buildVertices(VertexConsumer vertexConsumer, float z) {
|
||||
var quads = new FixedWidthFontRenderer.QuadEmitter(new Matrix4f().mul(pose).translate(0, 0, z), vertexConsumer);
|
||||
FixedWidthFontRenderer.drawTerminalBackground(quads, x, y, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderPipeline pipeline() {
|
||||
return RenderPipelines.TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
private record TerminalTextRenderState(
|
||||
int x, int y, Terminal terminal, Matrix3x2f pose, TextureSetup textureSetup,
|
||||
@Nullable ScreenRectangle bounds, @Nullable ScreenRectangle scissorArea
|
||||
) implements GuiElementRenderState {
|
||||
@Override
|
||||
public void buildVertices(VertexConsumer vertexConsumer, float z) {
|
||||
var quads = new FixedWidthFontRenderer.QuadEmitter(new Matrix4f().mul(pose).translate(0, 0, z), vertexConsumer);
|
||||
FixedWidthFontRenderer.drawTerminalForeground(quads, x, y, terminal);
|
||||
FixedWidthFontRenderer.drawCursor(quads, x, y, terminal);
|
||||
|
||||
// The GUI renderer requires that the buffer is non-empty. Add a zero-size vertex so we always have something.
|
||||
for (var i = 0; i < 4; i++) {
|
||||
vertexConsumer.addVertex(0, 0, z).setColor(0x00ffffff).setUv(0, 0).setLight(LightTexture.FULL_BRIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderPipeline pipeline() {
|
||||
return RenderPipelines.TEXT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,18 +5,16 @@
|
||||
package dan200.computercraft.client.integration;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.irisshaders.iris.api.v0.IrisApi;
|
||||
import net.irisshaders.iris.api.v0.IrisTextVertexSink;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
@AutoService(ShaderMod.Provider.class)
|
||||
public class IrisShaderMod implements ShaderMod.Provider {
|
||||
@@ -32,21 +30,20 @@ public class IrisShaderMod implements ShaderMod.Provider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, ByteBufferBuilder makeBuffer) {
|
||||
return IrisApi.getInstance().getMinorApiRevision() >= 1
|
||||
? new IrisQuadEmitter(vertexCount, makeBuffer)
|
||||
: super.getQuadEmitter(vertexCount, makeBuffer);
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return new IrisQuadEmitter(quadCount, makeBuffer);
|
||||
}
|
||||
|
||||
private static final class IrisQuadEmitter implements DirectFixedWidthFontRenderer.QuadEmitter {
|
||||
private static final class IrisQuadEmitter extends DirectFixedWidthFontRenderer.QuadEmitter {
|
||||
private final IrisTextVertexSink sink;
|
||||
private @Nullable ByteBuffer buffer;
|
||||
|
||||
private IrisQuadEmitter(int vertexCount, ByteBufferBuilder builder) {
|
||||
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, i -> {
|
||||
if (buffer != null) throw new IllegalStateException("Allocated multiple buffers");
|
||||
return buffer = MemoryUtil.memByteBuffer(builder.reserve(i), i);
|
||||
});
|
||||
private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> builder) {
|
||||
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer byteBuffer() {
|
||||
return sink.getUnderlyingByteBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -4,12 +4,14 @@
|
||||
|
||||
package dan200.computercraft.client.integration;
|
||||
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.VertexBuffer;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.TERMINAL_TEXT;
|
||||
|
||||
/**
|
||||
* Find the currently loaded shader mod (if present) and provides utilities for interacting with it.
|
||||
@@ -29,14 +31,14 @@ public class ShaderMod {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an appropriate quad emitter for use with {@link VertexBuffer} and {@link DirectFixedWidthFontRenderer} .
|
||||
* Get an appropriate quad emitter for use with a vertex buffer and {@link DirectFixedWidthFontRenderer} .
|
||||
*
|
||||
* @param vertexCount The number of vertices.
|
||||
* @param buffer A function to allocate a temporary buffer.
|
||||
* @param quadCount The number of quads.
|
||||
* @param makeBuffer A function to allocate a temporary buffer.
|
||||
* @return The quad emitter.
|
||||
*/
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, ByteBufferBuilder buffer) {
|
||||
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(buffer);
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(makeBuffer.apply(TERMINAL_TEXT.format().getVertexSize() * quadCount * 4));
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
|
@@ -8,13 +8,14 @@ import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.integration.RecipeModHelpers;
|
||||
import dan200.computercraft.shared.pocket.core.PocketSide;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import mezz.jei.api.IModPlugin;
|
||||
import mezz.jei.api.JeiPlugin;
|
||||
import mezz.jei.api.constants.RecipeTypes;
|
||||
import mezz.jei.api.constants.VanillaTypes;
|
||||
import mezz.jei.api.ingredients.subtypes.IIngredientSubtypeInterpreter;
|
||||
import mezz.jei.api.ingredients.subtypes.ISubtypeInterpreter;
|
||||
import mezz.jei.api.registration.IAdvancedRegistration;
|
||||
import mezz.jei.api.registration.ISubtypeRegistration;
|
||||
import mezz.jei.api.runtime.IJeiRuntime;
|
||||
@@ -46,7 +47,7 @@ public class JEIComputerCraft implements IModPlugin {
|
||||
|
||||
@Override
|
||||
public void registerAdvanced(IAdvancedRegistration registry) {
|
||||
registry.addRecipeManagerPlugin(new RecipeResolver(getRegistryAccess()));
|
||||
registry.addSimpleRecipeManagerPlugin(RecipeTypes.CRAFTING, new RecipeResolver(getRegistryAccess()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,7 +63,7 @@ public class JEIComputerCraft implements IModPlugin {
|
||||
// Hide all upgrade recipes
|
||||
var category = registry.createRecipeLookup(RecipeTypes.CRAFTING);
|
||||
category.get().forEach(wrapper -> {
|
||||
if (RecipeModHelpers.shouldRemoveRecipe(wrapper.id())) {
|
||||
if (RecipeModHelpers.shouldRemoveRecipe(wrapper.id().location())) {
|
||||
registry.hideRecipes(RecipeTypes.CRAFTING, List.of(wrapper));
|
||||
}
|
||||
});
|
||||
@@ -71,7 +72,7 @@ public class JEIComputerCraft implements IModPlugin {
|
||||
/**
|
||||
* Distinguishes turtles by upgrades and family.
|
||||
*/
|
||||
private static final IIngredientSubtypeInterpreter<ItemStack> turtleSubtype = (stack, ctx) -> {
|
||||
private static final ISubtypeInterpreter<ItemStack> turtleSubtype = (stack, ctx) -> {
|
||||
var name = new StringBuilder("turtle:");
|
||||
|
||||
// Add left and right upgrades to the identifier
|
||||
@@ -87,12 +88,15 @@ public class JEIComputerCraft implements IModPlugin {
|
||||
/**
|
||||
* Distinguishes pocket computers by upgrade and family.
|
||||
*/
|
||||
private static final IIngredientSubtypeInterpreter<ItemStack> pocketSubtype = (stack, ctx) -> {
|
||||
private static final ISubtypeInterpreter<ItemStack> pocketSubtype = (stack, ctx) -> {
|
||||
var name = new StringBuilder("pocket:");
|
||||
|
||||
// Add the upgrade to the identifier
|
||||
var upgrade = PocketComputerItem.getUpgradeWithData(stack);
|
||||
if (upgrade != null) name.append(upgrade.holder().key().location());
|
||||
var back = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BACK);
|
||||
var bottom = PocketComputerItem.getUpgradeWithData(stack, PocketSide.BOTTOM);
|
||||
if (back != null) name.append(back.holder().key().location());
|
||||
if (back != null && bottom != null) name.append('|');
|
||||
if (bottom != null) name.append(bottom.holder().key().location());
|
||||
|
||||
return name.toString();
|
||||
};
|
||||
@@ -100,7 +104,7 @@ public class JEIComputerCraft implements IModPlugin {
|
||||
/**
|
||||
* Distinguishes disks by colour.
|
||||
*/
|
||||
private static final IIngredientSubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DyedItemColor.getOrDefault(stack, -1));
|
||||
private static final ISubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DyedItemColor.getOrDefault(stack, -1));
|
||||
|
||||
private static RegistryAccess getRegistryAccess() {
|
||||
return Minecraft.getInstance().level.registryAccess();
|
||||
|
@@ -8,71 +8,90 @@ import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.integration.UpgradeRecipeGenerator;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import mezz.jei.api.constants.RecipeTypes;
|
||||
import mezz.jei.api.recipe.IFocus;
|
||||
import mezz.jei.api.recipe.RecipeType;
|
||||
import mezz.jei.api.recipe.advanced.IRecipeManagerPlugin;
|
||||
import mezz.jei.api.recipe.category.IRecipeCategory;
|
||||
import mezz.jei.api.ingredients.ITypedIngredient;
|
||||
import mezz.jei.api.recipe.advanced.ISimpleRecipeManagerPlugin;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.crafting.CraftingRecipe;
|
||||
import net.minecraft.world.item.crafting.RecipeHolder;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.item.crafting.display.RecipeDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class RecipeResolver implements IRecipeManagerPlugin {
|
||||
private final UpgradeRecipeGenerator<RecipeHolder<CraftingRecipe>> resolver;
|
||||
|
||||
class RecipeResolver implements ISimpleRecipeManagerPlugin<RecipeHolder<CraftingRecipe>> {
|
||||
/**
|
||||
* We need to generate unique ids for each recipe, as JEI will attempt to deduplicate them otherwise.
|
||||
*/
|
||||
private int nextId = 0;
|
||||
|
||||
private final UpgradeRecipeGenerator<RecipeHolder<CraftingRecipe>> resolver;
|
||||
|
||||
RecipeResolver(HolderLookup.Provider registries) {
|
||||
resolver = new UpgradeRecipeGenerator<>(
|
||||
x -> new RecipeHolder<>(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "upgrade_" + nextId++), x),
|
||||
registries
|
||||
);
|
||||
resolver = new UpgradeRecipeGenerator<>(x -> new RecipeHolder<>(
|
||||
ResourceKey.create(Registries.RECIPE, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "upgrade_" + nextId++)),
|
||||
new CraftingWrapper(x)
|
||||
), registries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> List<RecipeType<?>> getRecipeTypes(IFocus<V> focus) {
|
||||
var value = focus.getTypedValue().getIngredient();
|
||||
if (!(value instanceof ItemStack stack)) return List.of();
|
||||
|
||||
return switch (focus.getRole()) {
|
||||
case INPUT ->
|
||||
stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem || resolver.isUpgrade(stack)
|
||||
? List.of(RecipeTypes.CRAFTING)
|
||||
: List.of();
|
||||
case OUTPUT -> stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem
|
||||
? List.of(RecipeTypes.CRAFTING)
|
||||
: List.of();
|
||||
default -> List.of();
|
||||
};
|
||||
public boolean isHandledInput(ITypedIngredient<?> input) {
|
||||
return input.getIngredient() instanceof ItemStack stack
|
||||
&& (stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem || resolver.isUpgrade(stack));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, V> List<T> getRecipes(IRecipeCategory<T> recipeCategory, IFocus<V> focus) {
|
||||
if (!(focus.getTypedValue().getIngredient() instanceof ItemStack stack) || recipeCategory.getRecipeType() != RecipeTypes.CRAFTING) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return switch (focus.getRole()) {
|
||||
case INPUT -> cast(RecipeTypes.CRAFTING, resolver.findRecipesWithInput(stack));
|
||||
case OUTPUT -> cast(RecipeTypes.CRAFTING, resolver.findRecipesWithOutput(stack));
|
||||
default -> List.of();
|
||||
};
|
||||
public boolean isHandledOutput(ITypedIngredient<?> output) {
|
||||
return output.getIngredient() instanceof ItemStack stack
|
||||
&& (stack.getItem() instanceof TurtleItem || stack.getItem() instanceof PocketComputerItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> getRecipes(IRecipeCategory<T> recipeCategory) {
|
||||
public List<RecipeHolder<CraftingRecipe>> getRecipesForInput(ITypedIngredient<?> input) {
|
||||
return input.getIngredient() instanceof ItemStack stack ? resolver.findRecipesWithInput(stack) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecipeHolder<CraftingRecipe>> getRecipesForOutput(ITypedIngredient<?> output) {
|
||||
return output.getIngredient() instanceof ItemStack stack ? resolver.findRecipesWithOutput(stack) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecipeHolder<CraftingRecipe>> getAllRecipes() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes", "UnusedVariable" })
|
||||
private static <T, U> List<T> cast(RecipeType<U> ignoredType, List<U> from) {
|
||||
return (List) from;
|
||||
private record CraftingWrapper(RecipeDisplay recipes) implements CraftingRecipe {
|
||||
@Override
|
||||
public RecipeSerializer<? extends CraftingRecipe> getSerializer() {
|
||||
throw new IllegalStateException("Should not serialise CraftingWrapper");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CraftingBookCategory category() {
|
||||
return CraftingBookCategory.MISC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CraftingInput input, Level level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack assemble(CraftingInput input, HolderLookup.Provider registries) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlacementInfo placementInfo() {
|
||||
return PlacementInfo.NOT_PLACEABLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecipeDisplay> display() {
|
||||
return List.of(recipes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,32 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.item.model;
|
||||
|
||||
import net.minecraft.client.renderer.Sheets;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.DelegateBakedModel;
|
||||
|
||||
/**
|
||||
* A {@link BakedModel} that wraps another model, but providing different {@link ItemTransforms}.
|
||||
*/
|
||||
class BakedModelWithTransform extends DelegateBakedModel {
|
||||
private final ItemTransforms transforms;
|
||||
|
||||
BakedModelWithTransform(BakedModel bakedModel, ItemTransforms transforms) {
|
||||
super(bakedModel);
|
||||
this.transforms = transforms;
|
||||
}
|
||||
|
||||
static void addLayer(ItemStackRenderState state, BakedModel model, ItemTransforms transforms) {
|
||||
state.newLayer().setupBlockModel(new BakedModelWithTransform(model, transforms), Sheets.translucentItemSheet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemTransforms getTransforms() {
|
||||
return transforms;
|
||||
}
|
||||
}
|
@@ -7,8 +7,8 @@ package dan200.computercraft.client.item.model;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
@@ -39,8 +39,12 @@ public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel
|
||||
var overlay = TurtleItem.getOverlay(stack);
|
||||
if (overlay == null) return;
|
||||
|
||||
var model = ClientPlatformHelper.get().getModel(Minecraft.getInstance().getModelManager(), overlay.model());
|
||||
BakedModelWithTransform.addLayer(state, model, transforms());
|
||||
state.appendModelIdentityElement(this);
|
||||
state.appendModelIdentityElement(overlay);
|
||||
|
||||
var layer = state.newLayer();
|
||||
TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), overlay).model().setupItemLayer(layer);
|
||||
layer.setTransform(transforms().getTransform(context));
|
||||
}
|
||||
|
||||
public record Unbaked(ResourceLocation base) implements ItemModel.Unbaked {
|
||||
@@ -51,7 +55,7 @@ public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel
|
||||
|
||||
@Override
|
||||
public ItemModel bake(BakingContext bakingContext) {
|
||||
return new TurtleOverlayModel(bakingContext.bake(base).getTransforms());
|
||||
return new TurtleOverlayModel(bakingContext.blockModelBaker().getModel(base).getTopTransforms());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -4,23 +4,18 @@
|
||||
|
||||
package dan200.computercraft.client.item.model;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Transformation;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
@@ -28,12 +23,12 @@ import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An {@link ItemModel} that renders a turtle upgrade, using its {@link TurtleUpgradeModeller}.
|
||||
* An {@link ItemModel} that renders a turtle upgrade, using its {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModel}.
|
||||
*
|
||||
* @param side The side the upgrade resides on.
|
||||
* @param base The base model. Only used to provide item transforms.
|
||||
*/
|
||||
public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements ItemModel {
|
||||
public record TurtleUpgradeModel(TurtleSide side, ItemTransforms base) implements ItemModel {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/upgrade");
|
||||
public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
TurtleSide.CODEC.fieldOf("side").forGetter(Unbaked::side),
|
||||
@@ -41,21 +36,12 @@ public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements It
|
||||
).apply(instance, Unbaked::new));
|
||||
|
||||
@Override
|
||||
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int light) {
|
||||
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int seed) {
|
||||
var upgrade = TurtleItem.getUpgradeWithData(stack, side);
|
||||
if (upgrade == null) return;
|
||||
|
||||
switch (TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side)) {
|
||||
case TransformedModel.Item model -> {
|
||||
var childState = new ItemStackRenderState();
|
||||
resolver.updateForTopItem(childState, model.stack(), ItemDisplayContext.NONE, false, level, null, 0);
|
||||
if (!childState.isEmpty()) {
|
||||
state.newLayer().setupSpecialModel(new TransformedRenderer(childState, model.transformation()), null, base);
|
||||
}
|
||||
}
|
||||
case TransformedModel.Baked baked ->
|
||||
BakedModelWithTransform.addLayer(state, baked.model(), base.getTransforms());
|
||||
}
|
||||
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
|
||||
.renderForItem(upgrade, side, state, resolver, base.getTransform(context), seed);
|
||||
}
|
||||
|
||||
public record Unbaked(TurtleSide side, ResourceLocation base) implements ItemModel.Unbaked {
|
||||
@@ -66,29 +52,12 @@ public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements It
|
||||
|
||||
@Override
|
||||
public ItemModel bake(BakingContext bakingContext) {
|
||||
return new TurtleUpgradeModel(side, bakingContext.bake(base));
|
||||
return new TurtleUpgradeModel(side, bakingContext.blockModelBaker().getModel(base).getTopTransforms());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
resolver.resolve(base);
|
||||
}
|
||||
}
|
||||
|
||||
private record TransformedRenderer(
|
||||
ItemStackRenderState state, Transformation transform
|
||||
) implements SpecialModelRenderer<Void> {
|
||||
@Override
|
||||
public void render(@Nullable Void object, ItemDisplayContext itemDisplayContext, PoseStack poseStack, MultiBufferSource multiBufferSource, int overlay, int light, boolean bl) {
|
||||
poseStack.pushPose();
|
||||
poseStack.mulPose(transform.getMatrix());
|
||||
state.render(poseStack, multiBufferSource, overlay, light);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void extractArgument(ItemStack itemStack) {
|
||||
return null;
|
||||
resolver.markDependency(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.client.item.properties;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
@@ -38,6 +39,11 @@ public final class PocketComputerStateProperty implements SelectItemModelPropert
|
||||
return computer == null ? ComputerState.OFF : computer.getState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Codec<ComputerState> valueCodec() {
|
||||
return ComputerState.CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends SelectItemModelProperty<ComputerState>, ComputerState> type() {
|
||||
return TYPE;
|
||||
|
@@ -6,8 +6,10 @@ package dan200.computercraft.client.item.properties;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -29,7 +31,7 @@ public class TurtleShowElfOverlay implements ConditionalItemModelProperty {
|
||||
|
||||
@Override
|
||||
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
|
||||
var overlay = TurtleItem.getOverlay(stack);
|
||||
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
|
||||
return overlay == null || overlay.showElfOverlay();
|
||||
}
|
||||
|
||||
|
@@ -1,68 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A list of extra models to load on the client.
|
||||
* <p>
|
||||
* This is largely intended for use with {@linkplain TurtleOverlay turtle overlays}. As overlays are stored in a dynamic
|
||||
* registry, they are not available when resources are loaded, and so we need a way to request the overlays' models be
|
||||
* loaded.
|
||||
*
|
||||
* @param models The models to load.
|
||||
*/
|
||||
public record ExtraModels(List<ResourceLocation> models) {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExtraModels.class);
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
/**
|
||||
* The path where the extra models are listed.
|
||||
*/
|
||||
public static final ResourceLocation PATH = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "extra_models.json");
|
||||
|
||||
/**
|
||||
* The coded used to store the extra model file.
|
||||
*/
|
||||
public static final Codec<ExtraModels> CODEC = ResourceLocation.CODEC.listOf().xmap(ExtraModels::new, ExtraModels::models);
|
||||
|
||||
/**
|
||||
* Get the list of all extra models to load.
|
||||
*
|
||||
* @param resources The current resource manager.
|
||||
* @return A set of all resources to load.
|
||||
*/
|
||||
public static Collection<ResourceLocation> loadAll(ResourceManager resources) {
|
||||
Set<ResourceLocation> out = new HashSet<>();
|
||||
|
||||
for (var path : resources.getResourceStack(PATH)) {
|
||||
ExtraModels models;
|
||||
try (var stream = path.openAsReader()) {
|
||||
models = ExtraModels.CODEC.parse(JsonOps.INSTANCE, GSON.fromJson(stream, JsonElement.class)).getOrThrow(JsonParseException::new);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
LOG.error("Failed to load extra models from {}", path.sourcePackId());
|
||||
continue;
|
||||
}
|
||||
|
||||
out.addAll(models.models());
|
||||
}
|
||||
|
||||
return Collections.unmodifiableCollection(out);
|
||||
}
|
||||
}
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import dan200.computercraft.client.ClientTableFormatter;
|
||||
import dan200.computercraft.client.gui.AbstractComputerScreen;
|
||||
import dan200.computercraft.client.gui.OptionScreen;
|
||||
@@ -36,7 +35,6 @@ import java.util.UUID;
|
||||
/**
|
||||
* The client-side implementation of {@link ClientNetworkContext}.
|
||||
*/
|
||||
@AutoService(ClientNetworkContext.class)
|
||||
public final class ClientNetworkContextImpl implements ClientNetworkContext {
|
||||
@Override
|
||||
public void handleChatTable(TableBuilder table) {
|
||||
|
@@ -4,25 +4,38 @@
|
||||
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import dan200.computercraft.impl.Services;
|
||||
import net.minecraft.client.resources.model.ModelDebugName;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
|
||||
public interface ClientPlatformHelper {
|
||||
static ClientPlatformHelper get() {
|
||||
return (ClientPlatformHelper) dan200.computercraft.impl.client.ClientPlatformHelper.get();
|
||||
var instance = Instance.INSTANCE;
|
||||
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a {@link BakedModel}, using any loader-specific hooks.
|
||||
* Create a new unique {@link ModelKey}.
|
||||
*
|
||||
* @param transform The current matrix transformation to apply.
|
||||
* @param buffers The current pool of render buffers.
|
||||
* @param model The model to draw.
|
||||
* @param lightmapCoord The current packed lightmap coordinate.
|
||||
* @param overlayLight The current overlay light.
|
||||
* @param tints Block colour tints to apply to the model.
|
||||
* @param name The debug name for this model key.
|
||||
* @param <T> The type of baked model.
|
||||
* @return The newly created model key.
|
||||
*/
|
||||
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, int @Nullable [] tints);
|
||||
@Contract("_ -> new")
|
||||
<T> ModelKey<T> createModelKey(ModelDebugName name);
|
||||
|
||||
final class Instance {
|
||||
static final @Nullable ClientPlatformHelper INSTANCE;
|
||||
static final @Nullable Throwable ERROR;
|
||||
|
||||
static {
|
||||
var helper = Services.tryLoad(ClientPlatformHelper.class);
|
||||
INSTANCE = helper.instance();
|
||||
ERROR = helper.error();
|
||||
}
|
||||
|
||||
private Instance() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A key used to identify extra/standalone models.
|
||||
*
|
||||
* @param <T> The type of baked model.
|
||||
*/
|
||||
public interface ModelKey<T> {
|
||||
/**
|
||||
* Lookup this model key in the model manager.
|
||||
*
|
||||
* @param manager The model manager.
|
||||
* @return The loaded model, or {@code null} if not available.
|
||||
*/
|
||||
@Nullable
|
||||
T get(ModelManager manager);
|
||||
}
|
@@ -29,7 +29,7 @@ public final class CableHighlightRenderer {
|
||||
*/
|
||||
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
|
||||
var pos = hit.getBlockPos();
|
||||
var world = camera.getEntity().getCommandSenderWorld();
|
||||
var world = camera.getEntity().level();
|
||||
|
||||
var state = world.getBlockState(pos);
|
||||
|
||||
|
@@ -1,17 +1,14 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
|
||||
import static dan200.computercraft.client.render.SpriteRenderer.u;
|
||||
import static dan200.computercraft.client.render.SpriteRenderer.v;
|
||||
import dan200.computercraft.client.gui.ComputerScreen;
|
||||
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
|
||||
|
||||
/**
|
||||
* Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or
|
||||
* Constants for the borders of computers, either for a {@linkplain ComputerScreen GUI} or
|
||||
* {@linkplain PocketItemRenderer in-hand pocket computers}.
|
||||
*/
|
||||
public final class ComputerBorderRenderer {
|
||||
@@ -21,55 +18,13 @@ public final class ComputerBorderRenderer {
|
||||
public static final int MARGIN = 2;
|
||||
|
||||
/**
|
||||
* The width of the terminal border.
|
||||
* The size of the terminal border.
|
||||
* <p>
|
||||
* This is only used for layout of elements within UI. When rendering, the size of the computer's border is
|
||||
* determined by its {@link GuiSpriteScaling}.
|
||||
*/
|
||||
public static final int BORDER = 12;
|
||||
|
||||
public static final int LIGHT_HEIGHT = 8;
|
||||
|
||||
private static final int TEX_SIZE = 36;
|
||||
|
||||
private ComputerBorderRenderer() {
|
||||
}
|
||||
|
||||
public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) {
|
||||
var endX = x + width;
|
||||
var endY = y + height;
|
||||
|
||||
var border = GuiSprites.get(textures.border());
|
||||
|
||||
// Top bar
|
||||
blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER);
|
||||
blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER);
|
||||
blitBorder(renderer, border, endX, y - BORDER, BORDER * 2, 0, BORDER, BORDER);
|
||||
|
||||
// Vertical bars
|
||||
blitBorder(renderer, border, x - BORDER, y, 0, BORDER, BORDER, height);
|
||||
blitBorder(renderer, border, endX, y, BORDER * 2, BORDER, BORDER, height);
|
||||
|
||||
// Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the
|
||||
// pocket computer's lights).
|
||||
if (withLight) {
|
||||
var pocketBottomTexture = textures.pocketBottom();
|
||||
if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
|
||||
var pocketBottom = GuiSprites.get(pocketBottomTexture);
|
||||
|
||||
renderer.blitHorizontalSliced(
|
||||
pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT,
|
||||
BORDER, BORDER, BORDER * 3
|
||||
);
|
||||
} else {
|
||||
blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER);
|
||||
blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER);
|
||||
blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) {
|
||||
renderer.blit(
|
||||
x, y, width, height,
|
||||
u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE),
|
||||
u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -12,13 +12,13 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.LecternRenderer;
|
||||
@@ -39,19 +39,16 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON
|
||||
public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternBlockEntity> {
|
||||
private static final int POCKET_TERMINAL_RENDER_DISTANCE = 32;
|
||||
|
||||
private final BlockEntityRenderDispatcher berDispatcher;
|
||||
private final LecternPrintoutModel printoutModel;
|
||||
private final LecternPocketModel pocketModel;
|
||||
|
||||
public CustomLecternRenderer(BlockEntityRendererProvider.Context context) {
|
||||
berDispatcher = context.getBlockEntityRenderDispatcher();
|
||||
|
||||
printoutModel = new LecternPrintoutModel();
|
||||
pocketModel = new LecternPocketModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) {
|
||||
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay, Vec3 camera) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5f, 1.0625f, 0.5f);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(-lectern.getBlockState().getValue(LecternBlock.FACING).getClockWise().toYRot()));
|
||||
@@ -59,9 +56,9 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
poseStack.translate(0, -0.125f, 0);
|
||||
|
||||
var item = lectern.getItem();
|
||||
if (item.getItem() instanceof PrintoutItem printout) {
|
||||
if (item.getItem() instanceof PrintoutItem) {
|
||||
var vertexConsumer = LecternPrintoutModel.MATERIAL.buffer(buffer, RenderType::entitySolid);
|
||||
if (printout.getType() == PrintoutItem.Type.BOOK) {
|
||||
if (item.is(ModRegistry.Items.PRINTED_BOOK.get())) {
|
||||
printoutModel.renderBook(poseStack, vertexConsumer, packedLight, packedOverlay);
|
||||
} else {
|
||||
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutData.getOrEmpty(item).pages());
|
||||
@@ -82,7 +79,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
// Either render the terminal or a black screen, depending on how close we are.
|
||||
var terminal = computer == null ? null : computer.getTerminal();
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT));
|
||||
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
|
||||
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(camera, POCKET_TERMINAL_RENDER_DISTANCE)) {
|
||||
renderPocketTerminal(poseStack, quadEmitter, terminal);
|
||||
} else {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);
|
||||
|
@@ -12,8 +12,7 @@ import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Additional render state attached to a {@link ItemFrameRenderState}.
|
||||
|
@@ -1,58 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.entity.ItemRenderer;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.util.ARGB;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utilities for rendering {@link BakedModel}s and {@link BakedQuad}s.
|
||||
*/
|
||||
public final class ModelRenderer {
|
||||
private ModelRenderer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of {@linkplain BakedQuad quads} to a buffer.
|
||||
* <p>
|
||||
* This is not intended to be used directly, but instead by {@link ClientPlatformHelper#renderBakedModel(PoseStack, MultiBufferSource, BakedModel, int, int, int[])}. The
|
||||
* implementation here is identical to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, int[], int, int)}.
|
||||
*
|
||||
* @param transform The current matrix transformation to apply.
|
||||
* @param buffer The buffer to draw to.
|
||||
* @param quads The quads to draw.
|
||||
* @param lightmapCoord The current packed lightmap coordinate.
|
||||
* @param overlayLight The current overlay light.
|
||||
* @param tints Block colour tints to apply to the model.
|
||||
*/
|
||||
public static void renderQuads(PoseStack transform, VertexConsumer buffer, List<BakedQuad> quads, int lightmapCoord, int overlayLight, int @Nullable [] tints) {
|
||||
var matrix = transform.last();
|
||||
|
||||
for (var bakedquad : quads) {
|
||||
float r = 1.0f, g = 1.0f, b = 1.0f, a = 1.0f;
|
||||
if (tints != null && bakedquad.isTinted()) {
|
||||
var idx = bakedquad.getTintIndex();
|
||||
if (idx >= 0 && idx < tints.length) {
|
||||
var tint = tints[bakedquad.getTintIndex()];
|
||||
r = ARGB.red(tint) / 255.0f;
|
||||
g = ARGB.green(tint) / 255.0f;
|
||||
b = ARGB.blue(tint) / 255.0f;
|
||||
a = ARGB.alpha(tint) / 255.0f;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.putBulkData(matrix, bakedquad, r, g, b, a, lightmapCoord, overlayLight);
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,15 +13,17 @@ import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.*;
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER;
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
|
||||
@@ -31,6 +33,11 @@ import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FON
|
||||
public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
public static final PocketItemRenderer INSTANCE = new PocketItemRenderer();
|
||||
|
||||
/**
|
||||
* The height of the pocket computer's light.
|
||||
*/
|
||||
private static final int LIGHT_HEIGHT = 8;
|
||||
|
||||
private PocketItemRenderer() {
|
||||
}
|
||||
|
||||
@@ -85,14 +92,69 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
}
|
||||
|
||||
private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
|
||||
var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
|
||||
var textures = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
|
||||
var spriteRenderer = new SpriteRenderer(transform, render, 0, light, colour);
|
||||
renderBorder(spriteRenderer, textures, width, height);
|
||||
}
|
||||
|
||||
var r = (colour >>> 16) & 0xFF;
|
||||
var g = (colour >>> 8) & 0xFF;
|
||||
var b = colour & 0xFF;
|
||||
private static void renderBorder(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int width, int height) {
|
||||
var sprites = Minecraft.getInstance().getGuiSprites();
|
||||
|
||||
var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderType.text(GuiSprites.TEXTURE)), 0, light, r, g, b);
|
||||
ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true);
|
||||
// Find our border, forcing it to be a nine-sliced texture.
|
||||
var borderSprite = sprites.getSprite(textures.border());
|
||||
var borderSlice = getSlice(sprites.getSpriteScaling(borderSprite), DEFAULT_BORDER);
|
||||
var borderBounds = borderSlice.border();
|
||||
|
||||
// And take the separate bottom bit of the pocket computer.
|
||||
var bottomTexture = textures.pocketBottom();
|
||||
if (bottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
|
||||
var bottomSprite = sprites.getSprite(bottomTexture);
|
||||
var bottomSlice = getSlice(sprites.getSpriteScaling(bottomSprite), DEFAULT_BOTTOM);
|
||||
var bottomBounds = bottomSlice.border();
|
||||
|
||||
// Now draw a nine-sliced texture, by stitching together the top parts of the border with the pocket bottom.
|
||||
|
||||
// Top bar
|
||||
renderer.blit(
|
||||
borderSprite, -borderBounds.left(), -borderBounds.top(), borderBounds.left(), borderBounds.top(),
|
||||
0, 0, borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
renderer.blitTiled(
|
||||
borderSprite, 0, -borderBounds.top(), width, borderBounds.top(),
|
||||
borderBounds.left(), 0, borderSlice.width() - borderBounds.left() - borderBounds.right(), borderBounds.top(),
|
||||
borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
renderer.blit(
|
||||
borderSprite, width, -borderBounds.top(), borderBounds.right(), borderBounds.top(),
|
||||
borderSlice.width() - borderBounds.right(), 0, borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
|
||||
// Vertical bars
|
||||
renderer.blitTiled(
|
||||
borderSprite, -borderBounds.left(), 0, borderBounds.left(), height,
|
||||
0, borderBounds.top(), borderBounds.left(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(),
|
||||
borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
renderer.blitTiled(
|
||||
borderSprite, width, 0, borderBounds.right(), height,
|
||||
borderSlice.width() - borderBounds.right(), borderBounds.top(), borderBounds.right(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(),
|
||||
borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
|
||||
// Bottom
|
||||
renderer.blit(
|
||||
bottomSprite, -bottomBounds.left(), height, bottomBounds.left(), bottomSlice.height(),
|
||||
0, 0, bottomSlice.width(), bottomSlice.height()
|
||||
);
|
||||
renderer.blitTiled(
|
||||
bottomSprite, 0, height, width, bottomSlice.height(),
|
||||
bottomBounds.left(), 0, bottomSlice.width() - bottomBounds.left() - bottomBounds.right(), bottomSlice.height(),
|
||||
bottomSlice.width(), bottomSlice.height()
|
||||
);
|
||||
renderer.blit(
|
||||
bottomSprite, width, height, bottomBounds.right(), bottomSlice.height(),
|
||||
bottomSlice.width() - bottomBounds.right(), 0, bottomSlice.width(), bottomSlice.height()
|
||||
);
|
||||
}
|
||||
|
||||
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
|
||||
@@ -103,4 +165,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
ARGB.opaque(colour), LightTexture.FULL_BRIGHT
|
||||
);
|
||||
}
|
||||
|
||||
private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice(
|
||||
36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12), false
|
||||
);
|
||||
|
||||
private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice(
|
||||
36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false
|
||||
);
|
||||
|
||||
private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) {
|
||||
return scaling instanceof GuiSpriteScaling.NineSlice slice ? slice : fallback;
|
||||
}
|
||||
}
|
||||
|
@@ -5,134 +5,71 @@
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A {@link GuiGraphics}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity
|
||||
* renderer).
|
||||
* A {@link GuiGraphics}-equivalent renders to a {@link VertexConsumer}. This is suitable for rendering outside of a
|
||||
* GUI, such as part of an entity renderer.
|
||||
* <p>
|
||||
* This batches all render calls together, though requires that all {@link TextureAtlasSprite}s are on the same sprite
|
||||
* sheet.
|
||||
*/
|
||||
public class SpriteRenderer {
|
||||
public static final ResourceLocation TEXTURE = ResourceLocation.withDefaultNamespace("textures/atlas/gui.png");
|
||||
|
||||
private final Matrix4f transform;
|
||||
private final VertexConsumer builder;
|
||||
private final MultiBufferSource buffers;
|
||||
private final int light;
|
||||
private final int z;
|
||||
private final int r, g, b;
|
||||
private final int colour;
|
||||
|
||||
public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) {
|
||||
public SpriteRenderer(Matrix4f transform, MultiBufferSource buffers, int z, int light, int colour) {
|
||||
this.transform = transform;
|
||||
this.builder = builder;
|
||||
this.buffers = buffers;
|
||||
this.z = z;
|
||||
this.light = light;
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.colour = colour;
|
||||
}
|
||||
|
||||
public static void inGui(GuiGraphics graphics, Consumer<SpriteRenderer> renderer) {
|
||||
graphics.drawSpecial(bufferSource -> renderer.accept(new SpriteRenderer(
|
||||
graphics.pose().last().pose(), bufferSource.getBuffer(RenderType.guiTextured(GuiSprites.TEXTURE)),
|
||||
0, LightTexture.FULL_BRIGHT, 255, 255, 255
|
||||
)));
|
||||
public void blit(TextureAtlasSprite sprite, int x0, int y0, int width, int height, int spriteX, int spriteY, int spriteWidth, int spriteHeight) {
|
||||
if (width == 0 || height == 0) return;
|
||||
|
||||
var x1 = x0 + width;
|
||||
var y1 = y0 + height;
|
||||
var u0 = sprite.getU((float) spriteX / spriteWidth);
|
||||
var u1 = sprite.getU((float) (spriteX + width) / spriteWidth);
|
||||
var v0 = sprite.getV((float) spriteY / spriteHeight);
|
||||
var v1 = sprite.getV((float) (spriteY + height) / spriteHeight);
|
||||
|
||||
var vertices = buffers.getBuffer(RenderType.text(sprite.atlasLocation()));
|
||||
vertices.addVertex(transform, x0, y1, z).setColor(colour).setUv(u0, v1).setLight(light);
|
||||
vertices.addVertex(transform, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
|
||||
vertices.addVertex(transform, x1, y0, z).setColor(colour).setUv(u1, v0).setLight(light);
|
||||
vertices.addVertex(transform, x0, y0, z).setColor(colour).setUv(u0, v0).setLight(light);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single sprite.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
*/
|
||||
public void blit(TextureAtlasSprite sprite, int x, int y, int width, int height) {
|
||||
blit(x, y, width, height, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
public void blitTiled(
|
||||
TextureAtlasSprite sprite,
|
||||
int x, int y, int width, int height,
|
||||
int tileX, int tileY, int tileWidth, int tileHeight, int spriteWidth, int spriteHeight
|
||||
) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
if (tileWidth <= 0 || tileHeight <= 0) {
|
||||
throw new IllegalArgumentException("Tiled sprite texture size must be positive, got " + tileWidth + "x" + tileHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiGraphics#blitNineSliced},
|
||||
* the middle texture is stretched rather than repeated.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param leftBorder The width of the left border.
|
||||
* @param rightBorder The width of the right border.
|
||||
* @param textureWidth The width of the whole texture.
|
||||
*/
|
||||
public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) {
|
||||
// TODO(1.21.4): Drive this from mcmeta files, like vanilla does.
|
||||
if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders");
|
||||
|
||||
var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth);
|
||||
var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth);
|
||||
|
||||
blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1());
|
||||
blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1());
|
||||
blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiGraphics#blitNineSliced},
|
||||
* the middle texture is stretched rather than repeated.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param topBorder The height of the top border.
|
||||
* @param bottomBorder The height of the bottom border.
|
||||
* @param textureHeight The height of the whole texture.
|
||||
*/
|
||||
public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) {
|
||||
// TODO(1.21.4): Drive this from mcmeta files, like vanilla does.
|
||||
if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders");
|
||||
|
||||
var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight);
|
||||
var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight);
|
||||
|
||||
blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart);
|
||||
blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd);
|
||||
blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite.
|
||||
*
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param u0 The first U coordinate.
|
||||
* @param v0 The first V coordinate.
|
||||
* @param u1 The second U coordinate.
|
||||
* @param v1 The second V coordinate.
|
||||
*/
|
||||
public void blit(
|
||||
int x, int y, int width, int height, float u0, float v0, float u1, float v1) {
|
||||
builder.addVertex(transform, x, y + height, z).setColor(r, g, b, 255).setUv(u0, v1).setLight(light);
|
||||
builder.addVertex(transform, x + width, y + height, z).setColor(r, g, b, 255).setUv(u1, v1).setLight(light);
|
||||
builder.addVertex(transform, x + width, y, z).setColor(r, g, b, 255).setUv(u1, v0).setLight(light);
|
||||
builder.addVertex(transform, x, y, z).setColor(r, g, b, 255).setUv(u0, v0).setLight(light);
|
||||
}
|
||||
|
||||
public static float u(TextureAtlasSprite sprite, int x, int width) {
|
||||
return sprite.getU((float) x / width);
|
||||
}
|
||||
|
||||
public static float v(TextureAtlasSprite sprite, int y, int height) {
|
||||
return sprite.getV((float) y / height);
|
||||
for (var xOffset = 0; xOffset < width; xOffset += tileWidth) {
|
||||
var sliceWidth = Math.min(tileWidth, width - xOffset);
|
||||
for (var yOffset = 0; yOffset < height; yOffset += tileHeight) {
|
||||
var sliceHeight = Math.min(tileHeight, height - yOffset);
|
||||
blit(sprite, x + xOffset, y + yOffset, sliceWidth, sliceHeight, tileX, tileY, spriteWidth, spriteHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,12 +7,12 @@ package dan200.computercraft.client.render;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.client.ClientRegistry;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -21,14 +21,12 @@ import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.CommonColors;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
|
||||
@@ -45,7 +43,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight) {
|
||||
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, Vec3 camera) {
|
||||
transform.pushPose();
|
||||
|
||||
// Translate the turtle first, so the label moves with it.
|
||||
@@ -67,8 +65,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
var matrix = transform.last().pose();
|
||||
var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
|
||||
var width = -font.width(label) / 2.0f;
|
||||
font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
|
||||
font.drawInBatch(label, width, (float) 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
|
||||
font.drawInBatch(label, width, 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
|
||||
font.drawInBatch(label, width, 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
@@ -81,7 +79,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
|
||||
// Render the turtle
|
||||
var colour = turtle.getColour();
|
||||
var overlay = turtle.getOverlay();
|
||||
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
|
||||
|
||||
if (colour == -1) {
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
|
||||
@@ -91,10 +89,10 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
}
|
||||
|
||||
// Render the overlay
|
||||
if (overlay != null) renderModel(transform, buffers, lightmapCoord, overlayLight, overlay.model(), null);
|
||||
if (overlay != null) overlay.model().render(transform, buffers, lightmapCoord, overlayLight);
|
||||
|
||||
// And the Christmas overlay.
|
||||
var showChristmas = TurtleOverlay.showElfOverlay(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
|
||||
var showChristmas = Holiday.getCurrent() == Holiday.CHRISTMAS && (overlay == null || overlay.showElfOverlay());
|
||||
if (showChristmas) renderModel(transform, buffers, lightmapCoord, overlayLight, TurtleOverlay.ELF_MODEL, null);
|
||||
|
||||
// Render the upgrades
|
||||
@@ -105,7 +103,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
}
|
||||
|
||||
private void renderUpgrade(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
|
||||
var upgrade = turtle.getUpgrade(side);
|
||||
var upgrade = turtle.getAccess().getUpgradeWithData(side);
|
||||
if (upgrade == null) return;
|
||||
transform.pushPose();
|
||||
|
||||
@@ -114,40 +112,15 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
|
||||
transform.translate(0.0f, -0.5f, -0.5f);
|
||||
|
||||
switch (TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side)) {
|
||||
case TransformedModel.Item model -> {
|
||||
transform.mulPose(model.transformation().getMatrix());
|
||||
transform.mulPose(Axis.YP.rotation(Mth.PI));
|
||||
Minecraft.getInstance().getItemRenderer().renderStatic(
|
||||
model.stack(), ItemDisplayContext.FIXED, lightmapCoord, overlayLight, transform, buffers, turtle.getLevel(), 0
|
||||
);
|
||||
}
|
||||
|
||||
case TransformedModel.Baked model ->
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
|
||||
}
|
||||
|
||||
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
|
||||
.renderForLevel(upgrade, side, turtle.getAccess(), transform, buffers, lightmapCoord, overlayLight);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, int @Nullable [] tints) {
|
||||
var modelManager = Minecraft.getInstance().getModelManager();
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
|
||||
ClientRegistry.getModel(modelManager, modelLocation).render(transform, buffers, lightmapCoord, overlayLight, tints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block model.
|
||||
*
|
||||
* @param transform The current matrix stack.
|
||||
* @param renderer The buffer to write to.
|
||||
* @param lightmapCoord The current lightmap coordinate.
|
||||
* @param overlayLight The overlay light.
|
||||
* @param model The model to render.
|
||||
* @param tints Tints for the quads, as an array of RGB values.
|
||||
* @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel
|
||||
*/
|
||||
private void renderModel(PoseStack transform, MultiBufferSource renderer, int lightmapCoord, int overlayLight, BakedModel model, int @Nullable [] tints) {
|
||||
ClientPlatformHelper.get().renderBakedModel(transform, renderer, model, lightmapCoord, overlayLight, tints);
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,10 @@
|
||||
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.buffers.GpuBuffer;
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.*;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.client.FrameInfo;
|
||||
@@ -17,20 +19,26 @@ import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.client.renderer.CompiledShaderProgram;
|
||||
import net.minecraft.client.renderer.FogParameters;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.fog.FogRenderer;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBlockEntity> {
|
||||
/**
|
||||
@@ -39,13 +47,13 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
*/
|
||||
private static final float MARGIN = (float) (MonitorBlockEntity.RENDER_MARGIN * 1.1);
|
||||
|
||||
private static final ByteBufferBuilder backingBufferBuilder = new ByteBufferBuilder(0x4000);
|
||||
private static @Nullable ByteBuffer backingBuffer;
|
||||
|
||||
public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight, Vec3 camera) {
|
||||
// Render from the origin monitor
|
||||
var originTerminal = monitor.getOriginClientMonitor();
|
||||
if (originTerminal == null) return;
|
||||
@@ -122,27 +130,63 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin
|
||||
) {
|
||||
var redraw = monitor.pollTerminalChanged();
|
||||
if (renderState.createBuffer()) redraw = true;
|
||||
if (renderState.vertexBuffer == null) redraw = true;
|
||||
|
||||
var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
|
||||
var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
|
||||
if (redraw) {
|
||||
var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
|
||||
// Cursor, Foreground, Background+Margin
|
||||
var maxQuadCount = 1 + (terminal.getWidth() * terminal.getHeight()) + ((terminal.getWidth() + 2) * (terminal.getHeight() + 2));
|
||||
var maxVertexCount = 4 * maxQuadCount;
|
||||
var sink = ShaderMod.get().getQuadEmitter(maxQuadCount, MonitorBlockEntityRenderer::getBuffer);
|
||||
|
||||
// In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
|
||||
// a starting and ending offset, and so need to use two buffers instead.
|
||||
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin);
|
||||
var vertexCountAfterBackground = sink.vertexCount();
|
||||
|
||||
renderToBuffer(backgroundBuffer, size, sink ->
|
||||
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
|
||||
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
|
||||
var vertexCountAfterForeground = sink.vertexCount();
|
||||
|
||||
renderToBuffer(foregroundBuffer, size + 4, sink -> {
|
||||
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
|
||||
// If the cursor is visible, we append it to the end of our buffer. When rendering, we can either
|
||||
// render n or n+1 quads and so toggle the cursor on and off.
|
||||
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
|
||||
});
|
||||
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
|
||||
var vertexCountAfterCursor = sink.vertexCount();
|
||||
|
||||
if (vertexCountAfterCursor > maxVertexCount) {
|
||||
throw new IllegalStateException("Drew too many vertices. Expected " + maxVertexCount + ", drew " + vertexCountAfterCursor);
|
||||
}
|
||||
|
||||
if (vertexCountAfterCursor != 0) {
|
||||
renderState.register();
|
||||
|
||||
var commandEncoder = RenderSystem.getDevice().createCommandEncoder();
|
||||
|
||||
var resultBuffer = sink.byteBuffer().flip();
|
||||
|
||||
// Ensure our buffer contains the correct number of vertices.
|
||||
if (resultBuffer.remaining() != sink.format().getVertexSize() * vertexCountAfterCursor) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Mismatched vertex count. Buffer is %d bytes long, but was expected to be %d (vertex size) * %d (vertex count) = %d bytes.",
|
||||
resultBuffer.limit(), sink.format().getVertexSize(), vertexCountAfterCursor, sink.format().getVertexSize() * vertexCountAfterCursor
|
||||
));
|
||||
}
|
||||
|
||||
// Upload the buffer, reallocating if required.
|
||||
if (renderState.vertexBuffer == null || resultBuffer.remaining() > renderState.vertexBuffer.size()) {
|
||||
if (renderState.vertexBuffer != null) {
|
||||
renderState.vertexBuffer.close();
|
||||
renderState.vertexBuffer = null;
|
||||
}
|
||||
renderState.vertexBuffer = RenderSystem.getDevice().createBuffer(
|
||||
() -> "Monitor at " + monitor.getOrigin().getBlockPos(), GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, resultBuffer
|
||||
);
|
||||
} else if (!renderState.vertexBuffer.isClosed()) {
|
||||
commandEncoder.writeToBuffer(renderState.vertexBuffer.slice(), resultBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
renderState.vertexCountAfterBackground = vertexCountAfterBackground;
|
||||
renderState.vertexCountAfterForeground = vertexCountAfterForeground;
|
||||
renderState.vertexCountAfterCursor = vertexCountAfterCursor;
|
||||
}
|
||||
|
||||
if (renderState.vertexCountAfterCursor == 0) return;
|
||||
|
||||
// Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will
|
||||
// use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the
|
||||
// normal render distance (~200), and the edges of the monitor fade out due to fog.
|
||||
@@ -150,71 +194,72 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
// renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
|
||||
// absurdly high value.
|
||||
var oldFog = RenderSystem.getShaderFog();
|
||||
RenderSystem.setShaderFog(FogParameters.NO_FOG);
|
||||
|
||||
FixedWidthFontRenderer.TERMINAL_TEXT.setupRenderState();
|
||||
RenderSystem.setShaderFog(Minecraft.getInstance().gameRenderer.fogRenderer.getBuffer(FogRenderer.FogMode.NONE));
|
||||
|
||||
// Compose the existing model view matrix with our transformation matrix.
|
||||
var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix);
|
||||
RenderSystem.getModelViewStack().pushMatrix();
|
||||
RenderSystem.getModelViewStack().mul(matrix);
|
||||
|
||||
// Render background geometry
|
||||
backgroundBuffer.bind();
|
||||
backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderSystem.getShader());
|
||||
|
||||
// Render foreground geometry with glPolygonOffset enabled.
|
||||
RenderSystem.polygonOffset(-1.0f, -10.0f);
|
||||
RenderSystem.enablePolygonOffset();
|
||||
|
||||
foregroundBuffer.bind();
|
||||
drawWithShader(renderState, FixedWidthFontRenderer.TERMINAL_TEXT, RenderPipelines.TEXT, 0, renderState.vertexCountAfterBackground);
|
||||
drawWithShader(
|
||||
foregroundBuffer, modelView, RenderSystem.getProjectionMatrix(), RenderSystem.getShader(),
|
||||
// Skip the cursor quad if it is not visible this frame.
|
||||
FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
|
||||
? foregroundBuffer.indexCount - FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(4)
|
||||
: foregroundBuffer.indexCount
|
||||
renderState, FixedWidthFontRenderer.TERMINAL_TEXT_OFFSET, RenderPipelines.TEXT_POLYGON_OFFSET, renderState.vertexCountAfterBackground,
|
||||
(
|
||||
FixedWidthFontRenderer.isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()
|
||||
? renderState.vertexCountAfterCursor : renderState.vertexCountAfterForeground
|
||||
) - renderState.vertexCountAfterBackground
|
||||
);
|
||||
|
||||
// Clear state
|
||||
RenderSystem.polygonOffset(0.0f, -0.0f);
|
||||
RenderSystem.disablePolygonOffset();
|
||||
FixedWidthFontRenderer.TERMINAL_TEXT.clearRenderState();
|
||||
VertexBuffer.unbind();
|
||||
|
||||
RenderSystem.getModelViewStack().popMatrix();
|
||||
RenderSystem.setShaderFog(oldFog);
|
||||
}
|
||||
|
||||
private static void renderToBuffer(VertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
|
||||
var sink = ShaderMod.get().getQuadEmitter(size, backingBufferBuilder);
|
||||
draw.accept(sink);
|
||||
private static void drawWithShader(MonitorRenderState renderState, RenderType renderType, RenderPipeline pipeline, int vertexOffset, int vertexCount) {
|
||||
if (renderState.vertexBuffer == null) {
|
||||
throw new IllegalStateException("MonitorRenderState has not been initialised");
|
||||
}
|
||||
if (vertexCount == 0) return;
|
||||
|
||||
var result = backingBufferBuilder.build();
|
||||
if (result == null) {
|
||||
// If we have nothing to draw, just mark it as empty. We'll skip drawing in drawWithShader.
|
||||
vbo.indexCount = 0;
|
||||
return;
|
||||
var transforms = RenderSystem.getDynamicUniforms().writeTransform(
|
||||
RenderSystem.getModelViewMatrix(),
|
||||
new Vector4f(1.0F, 1.0F, 1.0F, 1.0F),
|
||||
RenderSystem.getModelOffset(),
|
||||
RenderSystem.getTextureMatrix(),
|
||||
RenderSystem.getShaderLineWidth()
|
||||
);
|
||||
|
||||
renderType.setupRenderState();
|
||||
|
||||
var autoStorageBuffer = RenderSystem.getSequentialBuffer(renderType.mode());
|
||||
var indexCount = FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(vertexCount);
|
||||
var indexBuffer = autoStorageBuffer.getBuffer(indexCount);
|
||||
|
||||
var target = Minecraft.getInstance().getMainRenderTarget();
|
||||
var colourTarget = RenderSystem.outputColorTextureOverride != null ? RenderSystem.outputColorTextureOverride : target.getColorTextureView();
|
||||
var depthTarget = target.useDepth
|
||||
? (RenderSystem.outputDepthTextureOverride != null ? RenderSystem.outputDepthTextureOverride : target.getDepthTextureView())
|
||||
: null;
|
||||
|
||||
try (var renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Monitor", colourTarget, OptionalInt.empty(), depthTarget, OptionalDouble.empty()
|
||||
)) {
|
||||
renderPass.setPipeline(pipeline);
|
||||
|
||||
RenderSystem.bindDefaultUniforms(renderPass);
|
||||
renderPass.setUniform("DynamicTransforms", transforms);
|
||||
renderPass.setVertexBuffer(0, renderState.vertexBuffer);
|
||||
renderPass.setIndexBuffer(indexBuffer, autoStorageBuffer.type());
|
||||
|
||||
for (var j = 0; j < 12; j++) {
|
||||
var gpuTexture = RenderSystem.getShaderTexture(j);
|
||||
if (gpuTexture != null) renderPass.bindSampler("Sampler" + j, gpuTexture);
|
||||
}
|
||||
|
||||
renderPass.drawIndexed(vertexOffset, 0, indexCount, 1);
|
||||
}
|
||||
|
||||
var buffer = result.byteBuffer();
|
||||
var vertices = buffer.limit() / sink.format().getVertexSize();
|
||||
|
||||
vbo.bind();
|
||||
vbo.upload(new MeshData(result, new MeshData.DrawState(
|
||||
sink.format(),
|
||||
vertices, FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(vertices),
|
||||
FixedWidthFontRenderer.TERMINAL_TEXT.mode(), VertexFormat.IndexType.least(vertices)
|
||||
)));
|
||||
}
|
||||
|
||||
private static void drawWithShader(VertexBuffer buffer, Matrix4f modelView, Matrix4f projection, @Nullable CompiledShaderProgram compiledShaderProgram, int indicies) {
|
||||
var originalIndexCount = buffer.indexCount;
|
||||
if (originalIndexCount == 0) return;
|
||||
|
||||
try {
|
||||
buffer.indexCount = indicies;
|
||||
buffer.drawWithShader(modelView, projection, compiledShaderProgram);
|
||||
} finally {
|
||||
buffer.indexCount = originalIndexCount;
|
||||
}
|
||||
renderType.clearRenderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -226,4 +271,14 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
public AABB getRenderBoundingBox(MonitorBlockEntity monitor) {
|
||||
return monitor.getRenderBoundingBox();
|
||||
}
|
||||
|
||||
private static ByteBuffer getBuffer(int capacity) {
|
||||
var buffer = backingBuffer;
|
||||
if (buffer == null || buffer.capacity() < capacity) {
|
||||
buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ public final class MonitorHighlightRenderer {
|
||||
// Preserve normal behaviour when crouching.
|
||||
if (camera.getEntity().isCrouching()) return false;
|
||||
|
||||
var world = camera.getEntity().getCommandSenderWorld();
|
||||
var world = camera.getEntity().level();
|
||||
var pos = hit.getBlockPos();
|
||||
|
||||
if (!(world.getBlockEntity(pos) instanceof MonitorBlockEntity monitor)) return false;
|
||||
|
@@ -5,8 +5,7 @@
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import com.mojang.blaze3d.buffers.BufferUsage;
|
||||
import com.mojang.blaze3d.vertex.VertexBuffer;
|
||||
import com.mojang.blaze3d.buffers.GpuBuffer;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -21,53 +20,39 @@ import java.util.Set;
|
||||
* This is automatically cleared by {@link dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity} when the
|
||||
* entity is unloaded on the client side (see {@link MonitorRenderState#close()}).
|
||||
*/
|
||||
public class MonitorRenderState implements ClientMonitor.RenderState {
|
||||
public final class MonitorRenderState implements ClientMonitor.RenderState {
|
||||
@GuardedBy("allMonitors")
|
||||
private static final Set<MonitorRenderState> allMonitors = new HashSet<>();
|
||||
|
||||
public long lastRenderFrame = -1;
|
||||
public @Nullable BlockPos lastRenderPos = null;
|
||||
long lastRenderFrame = -1;
|
||||
@Nullable
|
||||
BlockPos lastRenderPos = null;
|
||||
|
||||
public @Nullable VertexBuffer backgroundBuffer;
|
||||
public @Nullable VertexBuffer foregroundBuffer;
|
||||
@Nullable
|
||||
GpuBuffer vertexBuffer;
|
||||
|
||||
/**
|
||||
* Create the appropriate buffer if needed.
|
||||
*
|
||||
* @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer,
|
||||
* or this mode does not require one.
|
||||
*/
|
||||
public boolean createBuffer() {
|
||||
if (backgroundBuffer != null) return false;
|
||||
int vertexCountAfterBackground;
|
||||
int vertexCountAfterForeground;
|
||||
int vertexCountAfterCursor;
|
||||
|
||||
deleteBuffers();
|
||||
backgroundBuffer = new VertexBuffer(BufferUsage.STATIC_WRITE);
|
||||
foregroundBuffer = new VertexBuffer(BufferUsage.STATIC_WRITE);
|
||||
addMonitor();
|
||||
return true;
|
||||
}
|
||||
void register() {
|
||||
if (vertexBuffer != null) return;
|
||||
|
||||
private void addMonitor() {
|
||||
synchronized (allMonitors) {
|
||||
allMonitors.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBuffers() {
|
||||
if (backgroundBuffer != null) {
|
||||
backgroundBuffer.close();
|
||||
backgroundBuffer = null;
|
||||
}
|
||||
|
||||
if (foregroundBuffer != null) {
|
||||
foregroundBuffer.close();
|
||||
foregroundBuffer = null;
|
||||
if (vertexBuffer != null) {
|
||||
vertexBuffer.close();
|
||||
vertexBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (backgroundBuffer != null) {
|
||||
if (vertexBuffer != null) {
|
||||
synchronized (allMonitors) {
|
||||
allMonitors.remove(this);
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.client.render.text;
|
||||
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
@@ -15,6 +14,7 @@ import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.util.ARGB;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.*;
|
||||
@@ -33,7 +33,7 @@ import static org.lwjgl.system.MemoryUtil.*;
|
||||
* <p>
|
||||
* Note this is almost an exact copy of {@link FixedWidthFontRenderer}. While the code duplication is unfortunate,
|
||||
* it is measurably faster than introducing polymorphism into {@link FixedWidthFontRenderer}.
|
||||
*
|
||||
* <p>
|
||||
* <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
|
||||
* {@link FixedWidthFontRenderer}.
|
||||
*/
|
||||
@@ -156,21 +156,37 @@ public final class DirectFixedWidthFontRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public static int getVertexCount(Terminal terminal) {
|
||||
return (terminal.getHeight() + 2) * (terminal.getWidth() + 2) * 2;
|
||||
}
|
||||
|
||||
private static void quad(QuadEmitter buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
buffer.vertexCount += 4;
|
||||
buffer.quad(x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
|
||||
}
|
||||
|
||||
public interface QuadEmitter {
|
||||
VertexFormat format();
|
||||
public abstract static class QuadEmitter {
|
||||
private int vertexCount;
|
||||
|
||||
void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
|
||||
public abstract ByteBuffer byteBuffer();
|
||||
|
||||
public abstract VertexFormat format();
|
||||
|
||||
protected abstract void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
|
||||
|
||||
public int vertexCount() {
|
||||
return vertexCount;
|
||||
}
|
||||
}
|
||||
|
||||
public record ByteBufferEmitter(ByteBufferBuilder builder) implements QuadEmitter {
|
||||
public static final class ByteBufferEmitter extends QuadEmitter {
|
||||
private final ByteBuffer buffer;
|
||||
|
||||
public ByteBufferEmitter(ByteBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer byteBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexFormat format() {
|
||||
return TERMINAL_TEXT.format();
|
||||
@@ -178,17 +194,18 @@ public final class DirectFixedWidthFontRenderer {
|
||||
|
||||
@Override
|
||||
public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
DirectFixedWidthFontRenderer.quad(builder, x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
|
||||
DirectFixedWidthFontRenderer.quad(buffer, x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
|
||||
}
|
||||
}
|
||||
|
||||
static void quad(ByteBufferBuilder builder, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
private static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
// Emit a single quad to our buffer. This uses Unsafe (well, LWJGL's MemoryUtil) to directly blit bytes to the
|
||||
// underlying buffer. This allows us to have a single bounds check up-front, rather than one for every write.
|
||||
// This provides significant performance gains, at the cost of well, using Unsafe.
|
||||
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(abgr:BBBB)(uv1:FF)(uv2:SS),
|
||||
// which matches the POSITION_COLOR_TEX_LIGHTMAP vertex format.
|
||||
var addr = builder.reserve(112);
|
||||
var position = buffer.position();
|
||||
var addr = MemoryUtil.memAddress(buffer);
|
||||
|
||||
// We're doing terrible unsafe hacks below, so let's be really sure that what we're doing is reasonable.
|
||||
// Require the pointer to be aligned to a 32-bit boundary.
|
||||
@@ -237,6 +254,9 @@ public final class DirectFixedWidthFontRenderer {
|
||||
memPutShort(addr + 108, (short) 0xF0);
|
||||
memPutShort(addr + 110, (short) 0xF0);
|
||||
|
||||
// Finally increment the position.
|
||||
buffer.position(position + 112);
|
||||
|
||||
// Well done for getting to the end of this method. I recommend you take a break and go look at cute puppies.
|
||||
}
|
||||
}
|
||||
|
@@ -33,13 +33,15 @@ import org.joml.Vector3f;
|
||||
* {@link DirectFixedWidthFontRenderer}.
|
||||
*/
|
||||
public final class FixedWidthFontRenderer {
|
||||
private static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
|
||||
public static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
|
||||
|
||||
/**
|
||||
* A render type for terminal text.
|
||||
*/
|
||||
public static final RenderType TERMINAL_TEXT = RenderType.text(FONT);
|
||||
|
||||
public static final RenderType TERMINAL_TEXT_OFFSET = RenderType.textPolygonOffset(FONT);
|
||||
|
||||
public static final int FONT_HEIGHT = 9;
|
||||
public static final int FONT_WIDTH = 6;
|
||||
static final float WIDTH = 256.0f;
|
||||
|
@@ -48,7 +48,7 @@ public class SpeakerInstance {
|
||||
// Update the attenuation if the volume has changed: SoundEngine.tickNonPaused updates the volume
|
||||
// itself, but leaves the attenuation unchanged. We mirror the logic of SoundEngine.play here.
|
||||
if (volumeChanged) {
|
||||
channel.linearAttenuation(Math.max(volume, 1) * sound.getSound().getAttenuationDistance());
|
||||
channel.linearAttenuation(Math.max(volume, 1) * Nullability.assertNonNull(sound.getSound()).getAttenuationDistance());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -1,60 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
|
||||
import dan200.computercraft.shared.util.DataComponentUtil;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link TurtleUpgradeModeller} for modems, providing different models depending on if the modem is on/off.
|
||||
*/
|
||||
public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
|
||||
@Override
|
||||
public TransformedModel getModel(TurtleModem upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
|
||||
var active = DataComponentUtil.isPresent(data, ModRegistry.DataComponents.ON.get(), x -> x);
|
||||
|
||||
var models = upgrade.advanced() ? ModemModels.ADVANCED : ModemModels.NORMAL;
|
||||
return side == TurtleSide.LEFT
|
||||
? TransformedModel.of(active ? models.leftOnModel() : models.leftOffModel())
|
||||
: TransformedModel.of(active ? models.rightOnModel() : models.rightOffModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of(ModemModels.NORMAL, ModemModels.ADVANCED).flatMap(ModemModels::getDependencies);
|
||||
}
|
||||
|
||||
private record ModemModels(
|
||||
ResourceLocation leftOffModel, ResourceLocation rightOffModel,
|
||||
ResourceLocation leftOnModel, ResourceLocation rightOnModel
|
||||
) {
|
||||
private static final ModemModels NORMAL = create("normal");
|
||||
private static final ModemModels ADVANCED = create("advanced");
|
||||
|
||||
public static ModemModels create(String type) {
|
||||
return new ModemModels(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right")
|
||||
);
|
||||
}
|
||||
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.StandaloneModel;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.client.resources.model.ResolvableModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/**
|
||||
* A cosmetic overlay on a turtle.
|
||||
*
|
||||
* @param model The path to the overlay's model.
|
||||
* @param showElfOverlay Whether this overlay is compatible with the {@linkplain #ELF_MODEL Christmas elf model}.
|
||||
* @see ModRegistry.DataComponents#OVERLAY
|
||||
*/
|
||||
public record TurtleOverlay(StandaloneModel model, boolean showElfOverlay) {
|
||||
/**
|
||||
* The folder where upgrades are loaded from.
|
||||
*/
|
||||
public static final String SOURCE = ComputerCraftAPI.MOD_ID + "/turtle_overlay";
|
||||
|
||||
/**
|
||||
* The codec used to read/write turtle overlay definitions from resource packs.
|
||||
*/
|
||||
public static final Codec<TurtleOverlay.Unbaked> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
ResourceLocation.CODEC.fieldOf("model").forGetter(TurtleOverlay.Unbaked::model),
|
||||
Codec.BOOL.optionalFieldOf("show_elf_overlay", false).forGetter(TurtleOverlay.Unbaked::showElfOverlay)
|
||||
).apply(instance, TurtleOverlay.Unbaked::new));
|
||||
|
||||
/**
|
||||
* An additional overlay that is rendered on all turtles at {@linkplain Holiday#CHRISTMAS Christmas}.
|
||||
*
|
||||
* @see #showElfOverlay()
|
||||
*/
|
||||
public static final ResourceLocation ELF_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
|
||||
|
||||
public record Unbaked(ResourceLocation model, boolean showElfOverlay) implements ResolvableModel {
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
resolver.markDependency(model());
|
||||
}
|
||||
|
||||
public TurtleOverlay bake(ModelBaker baker) {
|
||||
return new TurtleOverlay(StandaloneModel.of(model(), baker), showElfOverlay());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import dan200.computercraft.client.CustomModelManager;
|
||||
import net.minecraft.client.resources.model.MissingBlockModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.resources.FileToIdConverter;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* The model manager for {@link TurtleOverlay}s.
|
||||
*/
|
||||
public class TurtleOverlayManager {
|
||||
private static final CustomModelManager<TurtleOverlay.Unbaked, TurtleOverlay> loader = new CustomModelManager<>(
|
||||
"turtle overlay", FileToIdConverter.json(TurtleOverlay.SOURCE), TurtleOverlay.CODEC,
|
||||
TurtleOverlay.Unbaked::bake,
|
||||
new TurtleOverlay.Unbaked(MissingBlockModel.LOCATION, false)
|
||||
);
|
||||
|
||||
|
||||
public static CustomModelManager<TurtleOverlay.Unbaked, TurtleOverlay> loader() {
|
||||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the turtle overlay with the given id. If the overlay does not exist, then the "missing model" overlay is
|
||||
* returned instead.
|
||||
*
|
||||
* @param modelManager The model manager.
|
||||
* @param id The overlay id.
|
||||
* @return The turtle overlay.
|
||||
*/
|
||||
@Contract("_, null -> null; _, !null -> !null")
|
||||
public static @Nullable TurtleOverlay get(ModelManager modelManager, @Nullable ResourceLocation id) {
|
||||
return id == null ? null : loader.get(modelManager, id);
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.client.turtle.BasicUpgradeModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.client.CustomModelManager;
|
||||
import net.minecraft.client.resources.model.MissingBlockModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.resources.FileToIdConverter;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* The model manager for {@link TurtleUpgradeModel}s.
|
||||
*/
|
||||
public final class TurtleUpgradeModelManager {
|
||||
private static final CustomModelManager<TurtleUpgradeModel.Unbaked, TurtleUpgradeModel> loader = new CustomModelManager<>(
|
||||
"turtle upgrade", FileToIdConverter.json(TurtleUpgradeModel.SOURCE), TurtleUpgradeModel.CODEC,
|
||||
TurtleUpgradeModel.Unbaked::bake,
|
||||
BasicUpgradeModel.unbaked(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION)
|
||||
);
|
||||
|
||||
public static CustomModelManager<TurtleUpgradeModel.Unbaked, TurtleUpgradeModel> loader() {
|
||||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the model for the given turtle upgrade.
|
||||
*
|
||||
* @param modelManager The model manager.
|
||||
* @param upgrade The turtle upgrade
|
||||
* @return The turtle upgrade model.
|
||||
*/
|
||||
@Contract("_, null -> null; _, !null -> !null")
|
||||
public static @Nullable TurtleUpgradeModel get(ModelManager modelManager, Holder.@Nullable Reference<ITurtleUpgrade> upgrade) {
|
||||
return upgrade == null ? null : loader.get(modelManager, upgrade.key().location());
|
||||
}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeType;
|
||||
import dan200.computercraft.shared.util.RegistryHelper;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A registry of {@link TurtleUpgradeModeller}s.
|
||||
*/
|
||||
public final class TurtleUpgradeModellers {
|
||||
private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side, data) ->
|
||||
TransformedModel.of(Minecraft.getInstance().getModelManager().getMissingModel());
|
||||
|
||||
private static final Map<UpgradeType<? extends ITurtleUpgrade>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
|
||||
private static volatile boolean fetchedModels;
|
||||
|
||||
private TurtleUpgradeModellers() {
|
||||
}
|
||||
|
||||
public static <T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModeller<T> modeller) {
|
||||
if (fetchedModels) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Turtle upgrade type %s must be registered before models are baked.",
|
||||
RegistryHelper.getKeyOrThrow(RegistryHelper.getRegistry(ITurtleUpgrade.typeRegistry()), type)
|
||||
));
|
||||
}
|
||||
|
||||
if (turtleModels.putIfAbsent(type, modeller) != null) {
|
||||
throw new IllegalStateException("Modeller already registered for serialiser");
|
||||
}
|
||||
}
|
||||
|
||||
public static TransformedModel getModel(ITurtleUpgrade upgrade, ITurtleAccess access, TurtleSide side) {
|
||||
return getModeller(upgrade).getModel(upgrade, access, side, access.getUpgradeData(side));
|
||||
}
|
||||
|
||||
public static TransformedModel getModel(ITurtleUpgrade upgrade, DataComponentPatch data, TurtleSide side) {
|
||||
return getModeller(upgrade).getModel(upgrade, null, side, data);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> getModeller(T upgrade) {
|
||||
var modeller = turtleModels.get(upgrade.getType());
|
||||
return (TurtleUpgradeModeller<T>) (modeller == null ? NULL_TURTLE_MODELLER : modeller);
|
||||
}
|
||||
|
||||
public static Stream<ResourceLocation> getDependencies() {
|
||||
fetchedModels = true;
|
||||
return turtleModels.values().stream().flatMap(TurtleUpgradeModeller::getDependencies);
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.ClientHooks;
|
||||
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.BlockAndTintGetter;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
|
||||
/**
|
||||
* Provides custom block breaking progress for modems, so it only applies to the current part.
|
||||
*
|
||||
* @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer)
|
||||
*/
|
||||
@Mixin(BlockRenderDispatcher.class)
|
||||
public class BlockRenderDispatcherMixin {
|
||||
@ModifyVariable(method = "renderBreakingTexture", at = @At("HEAD"))
|
||||
public BlockState renderBlockDamage(BlockState state, @Local BlockPos pos) {
|
||||
return ClientHooks.getBlockBreakingState(state, pos);
|
||||
}
|
||||
}
|
@@ -7,7 +7,6 @@
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"client": [
|
||||
"BlockRenderDispatcherMixin",
|
||||
"ClientPacketListenerMixin"
|
||||
"BlockRenderDispatcherMixin"
|
||||
]
|
||||
}
|
@@ -5,15 +5,15 @@
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.model.LecternPocketModel;
|
||||
import dan200.computercraft.client.model.LecternPrintoutModel;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.data.client.BlockModelProvider;
|
||||
import dan200.computercraft.data.client.ExtraModelsProvider;
|
||||
import dan200.computercraft.data.client.ItemModelProvider;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.data.models.BlockModelGenerators;
|
||||
@@ -21,6 +21,7 @@ import net.minecraft.client.data.models.ItemModelGenerators;
|
||||
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
|
||||
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
|
||||
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
|
||||
import net.minecraft.client.resources.model.AtlasIds;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistrySetBuilder;
|
||||
import net.minecraft.data.DataProvider;
|
||||
@@ -53,35 +54,30 @@ public final class DataProviders {
|
||||
var fullRegistryPatch = RegistryPatchGenerator.createLookup(
|
||||
generator.registries(),
|
||||
Util.make(new RegistrySetBuilder(), builder -> {
|
||||
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
|
||||
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::register);
|
||||
builder.add(IPocketUpgrade.REGISTRY, PocketUpgradeProvider::addUpgrades);
|
||||
builder.add(TurtleOverlay.REGISTRY, TurtleOverlays::register);
|
||||
}));
|
||||
var fullRegistries = fullRegistryPatch.thenApply(RegistrySetBuilder.PatchedRegistries::full);
|
||||
|
||||
generator.registries(fullRegistryPatch);
|
||||
generator.add(out -> new RecipeProvider.Runner(out, fullRegistries));
|
||||
|
||||
var blockTags = generator.blockTags(TagProvider::blockTags);
|
||||
generator.itemTags(TagProvider::itemTags, blockTags);
|
||||
generator.blockTags(TagProvider::blockTags);
|
||||
generator.itemTags(TagProvider::itemTags);
|
||||
|
||||
generator.add(out -> new net.minecraft.data.loot.LootTableProvider(out, Set.of(), LootTableProvider.getTables(), fullRegistries));
|
||||
|
||||
generator.add(out -> new LanguageProvider(out, fullRegistries));
|
||||
|
||||
generator.addFromCodec("Block atlases", PackOutput.Target.RESOURCE_PACK, "atlases", SpriteSources.FILE_CODEC, out -> {
|
||||
out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
|
||||
out.accept(AtlasIds.BLOCKS, makeSprites(Stream.of(
|
||||
LecternPrintoutModel.TEXTURE,
|
||||
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
|
||||
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
|
||||
)));
|
||||
|
||||
out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites(Stream.of(
|
||||
UpgradeSlot.LEFT_UPGRADE,
|
||||
UpgradeSlot.RIGHT_UPGRADE
|
||||
)));
|
||||
|
||||
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
|
||||
out.accept(AtlasIds.GUI, makeSprites(
|
||||
Stream.of(UpgradeSlot.LEFT_UPGRADE, UpgradeSlot.RIGHT_UPGRADE),
|
||||
// Computers
|
||||
GuiSprites.COMPUTER_NORMAL.textures(),
|
||||
GuiSprites.COMPUTER_ADVANCED.textures(),
|
||||
@@ -90,12 +86,10 @@ public final class DataProviders {
|
||||
));
|
||||
});
|
||||
|
||||
generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) {
|
||||
@Override
|
||||
public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
|
||||
return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
|
||||
}
|
||||
});
|
||||
generator.add(ResourceMetadataProvider::new);
|
||||
|
||||
generator.addFromCodec("Turtle overlays", PackOutput.Target.RESOURCE_PACK, TurtleOverlay.SOURCE, TurtleOverlay.CODEC, TurtleOverlays::register);
|
||||
generator.addFromCodec("Turtle upgrade models", PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC, TurtleUpgradeProvider::addModels);
|
||||
|
||||
generator.addModels(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
|
||||
}
|
||||
@@ -115,7 +109,7 @@ public final class DataProviders {
|
||||
|
||||
TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags);
|
||||
|
||||
TagsProvider<Item> itemTags(Consumer<TagProvider.ItemTagConsumer> tags, TagsProvider<Block> blocks);
|
||||
TagsProvider<Item> itemTags(Consumer<TagProvider.TagConsumer<Item>> tags);
|
||||
|
||||
/**
|
||||
* Build new dynamic registries and save them to a pack.
|
||||
|
@@ -100,14 +100,18 @@ public final class LanguageProvider implements DataProvider {
|
||||
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), "Pocket Computer");
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().getDescriptionId() + ".upgraded", "%s Pocket Computer");
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().getDescriptionId() + ".upgraded_twice", "%s %s Pocket Computer");
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(), "Advanced Pocket Computer");
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded", "Advanced %s Pocket Computer");
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded_twice", "Advanced %s %s Pocket Computer");
|
||||
|
||||
// Tags (for EMI)
|
||||
add(ComputerCraftTags.Items.COMPUTER, "Computers");
|
||||
add(ComputerCraftTags.Items.TURTLE, "Turtles");
|
||||
add(ComputerCraftTags.Items.WIRED_MODEM, "Wired modems");
|
||||
add(ComputerCraftTags.Items.MONITOR, "Monitors");
|
||||
add(ComputerCraftTags.Items.DISKS, "Disks");
|
||||
add(ComputerCraftTags.Items.POCKET_COMPUTERS, "Pocket Computers");
|
||||
add(ComputerCraftTags.Items.DYEABLE, "Dyable items");
|
||||
add(ComputerCraftTags.Items.TURTLE_CAN_PLACE, "Turtle-placeable items");
|
||||
|
||||
@@ -187,7 +191,6 @@ public final class LanguageProvider implements DataProvider {
|
||||
// Metrics
|
||||
add(Metrics.COMPUTER_TASKS, "Tasks");
|
||||
add(Metrics.SERVER_TASKS, "Server tasks");
|
||||
add(Metrics.JAVA_ALLOCATION, "Java Allocations");
|
||||
add(Metrics.PERIPHERAL_OPS, "Peripheral calls");
|
||||
add(Metrics.FS_OPS, "Filesystem operations");
|
||||
add(Metrics.HTTP_REQUESTS, "HTTP requests");
|
||||
|
@@ -24,7 +24,6 @@ import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
|
||||
import dan200.computercraft.shared.recipe.TransformShapedRecipe;
|
||||
import dan200.computercraft.shared.recipe.TransformShapelessRecipe;
|
||||
import dan200.computercraft.shared.recipe.function.CopyComponents;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
||||
import dan200.computercraft.shared.util.ColourUtils;
|
||||
@@ -141,7 +140,7 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
|
||||
registries.lookupOrThrow(IPocketUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
|
||||
var upgrade = upgradeHolder.value();
|
||||
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
|
||||
customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.BACK_POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
|
||||
.group(name.toString())
|
||||
.pattern("#")
|
||||
.pattern("P")
|
||||
@@ -178,13 +177,11 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
);
|
||||
}
|
||||
|
||||
private void turtleOverlay(ResourceKey<TurtleOverlay> overlay, Consumer<ShapelessSpecBuilder> build) {
|
||||
var holder = registries.lookupOrThrow(overlay.registryKey()).getOrThrow(overlay);
|
||||
|
||||
private void turtleOverlay(ResourceLocation overlay, Consumer<ShapelessSpecBuilder> build) {
|
||||
for (var turtleItem : turtleItems()) {
|
||||
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
|
||||
|
||||
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), holder))
|
||||
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), overlay))
|
||||
.group(name.withSuffix("_overlay").toString())
|
||||
.unlockedBy("has_turtle", has(turtleItem));
|
||||
build.accept(builder);
|
||||
@@ -193,7 +190,7 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
|
||||
.build(s -> new TransformShapelessRecipe(s, List.of(
|
||||
CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
|
||||
)))
|
||||
.save(output, name.withSuffix("_overlays/" + overlay.location().getPath()));
|
||||
.save(output, name.withSuffix("_overlays/" + overlay.getPath()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import net.minecraft.client.resources.metadata.gui.GuiMetadataSection;
|
||||
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
|
||||
import net.minecraft.data.CachedOutput;
|
||||
import net.minecraft.data.DataProvider;
|
||||
import net.minecraft.data.PackOutput;
|
||||
import net.minecraft.data.metadata.PackMetadataGenerator;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.metadata.MetadataSectionType;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Generates {@code .mcmeta} files for texture files.
|
||||
* <p>
|
||||
* This is similar to {@link PackMetadataGenerator}, but for individual resources.
|
||||
*/
|
||||
final class ResourceMetadataProvider implements DataProvider {
|
||||
private final PackOutput output;
|
||||
|
||||
ResourceMetadataProvider(PackOutput output) {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
private void register(Builder builder) {
|
||||
for (var computerTextures : List.of(
|
||||
GuiSprites.COMPUTER_ADVANCED,
|
||||
GuiSprites.COMPUTER_COLOUR,
|
||||
GuiSprites.COMPUTER_COMMAND,
|
||||
GuiSprites.COMPUTER_NORMAL
|
||||
)) {
|
||||
builder.texture(computerTextures.border()).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
|
||||
new GuiSpriteScaling.NineSlice(36, 36, simpleNineSlicedBorder(12), false)
|
||||
));
|
||||
|
||||
var sidebar = computerTextures.sidebar();
|
||||
if (sidebar != null) {
|
||||
builder.texture(sidebar).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
|
||||
new GuiSpriteScaling.NineSlice(17, 14, new GuiSpriteScaling.NineSlice.Border(3, 4, 0, 3), false)
|
||||
));
|
||||
}
|
||||
|
||||
var pocketBottom = computerTextures.pocketBottom();
|
||||
if (pocketBottom != null) {
|
||||
builder.texture(pocketBottom).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
|
||||
new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0), false)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static GuiSpriteScaling.NineSlice.Border simpleNineSlicedBorder(int size) {
|
||||
return new GuiSpriteScaling.NineSlice.Border(size, size, size, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> run(CachedOutput cachedOutput) {
|
||||
var builder = new Builder();
|
||||
register(builder);
|
||||
|
||||
var outputPath = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK);
|
||||
return CompletableFuture.allOf(builder.metadata.entrySet().stream().map(entry -> {
|
||||
var json = new JsonObject();
|
||||
entry.getValue().elements.forEach((name, element) -> json.add(name, element.get()));
|
||||
return DataProvider.saveStable(cachedOutput, json, outputPath.resolve(entry.getKey().getNamespace()).resolve(entry.getKey().getPath() + ".mcmeta"));
|
||||
}).toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Resource Metadata";
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for a set of {@code mcmeta} files.
|
||||
*/
|
||||
private static final class Builder {
|
||||
private final Map<ResourceLocation, FileMetadata> metadata = new HashMap<>();
|
||||
|
||||
FileMetadata texture(ResourceLocation texture) {
|
||||
return file(texture.withPrefix("textures/").withSuffix(".png"));
|
||||
}
|
||||
|
||||
FileMetadata file(ResourceLocation path) {
|
||||
return metadata.computeIfAbsent(path, p -> new FileMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for a given file's {@code mcmeta} file.
|
||||
*/
|
||||
private static final class FileMetadata {
|
||||
private final Map<String, Supplier<JsonElement>> elements = new HashMap<>();
|
||||
|
||||
<T> FileMetadata add(MetadataSectionType<T> type, T value) {
|
||||
elements.put(type.name(), () -> type.codec().encodeStart(JsonOps.INSTANCE, value).getOrThrow().getAsJsonObject());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,13 +7,10 @@ package dan200.computercraft.data;
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.integration.ExternalModTags;
|
||||
import dan200.computercraft.shared.util.RegistryHelper;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.data.tags.ItemTagsProvider;
|
||||
import net.minecraft.data.tags.TagAppender;
|
||||
import net.minecraft.data.tags.TagsProvider;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.tags.ItemTags;
|
||||
import net.minecraft.tags.TagBuilder;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.Items;
|
||||
@@ -28,14 +25,8 @@ import net.minecraft.world.level.block.Blocks;
|
||||
*/
|
||||
class TagProvider {
|
||||
public static void blockTags(TagConsumer<Block> tags) {
|
||||
tags.tag(ComputerCraftTags.Blocks.COMPUTER).add(
|
||||
ModRegistry.Blocks.COMPUTER_NORMAL.get(),
|
||||
ModRegistry.Blocks.COMPUTER_ADVANCED.get(),
|
||||
ModRegistry.Blocks.COMPUTER_COMMAND.get()
|
||||
);
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE).add(ModRegistry.Blocks.TURTLE_NORMAL.get(), ModRegistry.Blocks.TURTLE_ADVANCED.get());
|
||||
itemAndBlockTags((b, i) -> tags.tag(b));
|
||||
tags.tag(ComputerCraftTags.Blocks.WIRED_MODEM).add(ModRegistry.Blocks.CABLE.get(), ModRegistry.Blocks.WIRED_MODEM_FULL.get());
|
||||
tags.tag(ComputerCraftTags.Blocks.MONITOR).add(ModRegistry.Blocks.MONITOR_NORMAL.get(), ModRegistry.Blocks.MONITOR_ADVANCED.get());
|
||||
|
||||
tags.tag(ComputerCraftTags.Blocks.PERIPHERAL_HUB_IGNORE).addTag(ComputerCraftTags.Blocks.WIRED_MODEM);
|
||||
|
||||
@@ -91,11 +82,11 @@ class TagProvider {
|
||||
);
|
||||
}
|
||||
|
||||
public static void itemTags(ItemTagConsumer tags) {
|
||||
tags.copy(ComputerCraftTags.Blocks.COMPUTER, ComputerCraftTags.Items.COMPUTER);
|
||||
tags.copy(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE);
|
||||
public static void itemTags(TagConsumer<Item> tags) {
|
||||
itemAndBlockTags((b, i) -> tags.tag(i).map(Block::asItem));
|
||||
tags.tag(ComputerCraftTags.Items.WIRED_MODEM).add(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get());
|
||||
tags.copy(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR);
|
||||
tags.tag(ComputerCraftTags.Items.DISKS).add(ModRegistry.Items.DISK.get(), ModRegistry.Items.TREASURE_DISK.get());
|
||||
tags.tag(ComputerCraftTags.Items.POCKET_COMPUTERS).add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
|
||||
|
||||
tags.tag(ComputerCraftTags.Items.DYEABLE)
|
||||
.addTag(ComputerCraftTags.Items.TURTLE)
|
||||
@@ -115,37 +106,32 @@ class TagProvider {
|
||||
.addTag(ItemTags.BOATS);
|
||||
}
|
||||
|
||||
private static void itemAndBlockTags(BlockItemTagConsumer tags) {
|
||||
tags.tag(ComputerCraftTags.Blocks.COMPUTER, ComputerCraftTags.Items.COMPUTER).add(
|
||||
ModRegistry.Blocks.COMPUTER_NORMAL.get(),
|
||||
ModRegistry.Blocks.COMPUTER_ADVANCED.get(),
|
||||
ModRegistry.Blocks.COMPUTER_COMMAND.get()
|
||||
);
|
||||
tags.tag(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE).add(
|
||||
ModRegistry.Blocks.TURTLE_NORMAL.get(),
|
||||
ModRegistry.Blocks.TURTLE_ADVANCED.get()
|
||||
);
|
||||
tags.tag(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR).add(
|
||||
ModRegistry.Blocks.MONITOR_NORMAL.get(),
|
||||
ModRegistry.Blocks.MONITOR_ADVANCED.get()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper over {@link TagsProvider}.
|
||||
*
|
||||
* @param <T> The type of object we're providing tags for.
|
||||
*/
|
||||
public interface TagConsumer<T> {
|
||||
TagAppender<T> tag(TagKey<T> tag);
|
||||
TagAppender<T, T> tag(TagKey<T> tag);
|
||||
}
|
||||
|
||||
public record TagAppender<T>(Registry<T> registry, TagBuilder builder) {
|
||||
public TagAppender<T> add(T object) {
|
||||
builder.addElement(RegistryHelper.getKeyOrThrow(registry, object));
|
||||
return this;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final TagAppender<T> add(T... objects) {
|
||||
for (var object : objects) add(object);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TagAppender<T> addTag(TagKey<T> tag) {
|
||||
builder.addTag(tag.location());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper over {@link ItemTagsProvider}.
|
||||
*/
|
||||
public interface ItemTagConsumer extends TagConsumer<Item> {
|
||||
void copy(TagKey<Block> block, TagKey<Item> item);
|
||||
private interface BlockItemTagConsumer {
|
||||
TagAppender<Block, ?> tag(TagKey<Block> blockTag, TagKey<Item> itemTag);
|
||||
}
|
||||
}
|
||||
|
@@ -5,32 +5,32 @@
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import net.minecraft.data.worldgen.BootstrapContext;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Built-in turtle overlays.
|
||||
*/
|
||||
final class TurtleOverlays {
|
||||
public static final ResourceKey<TurtleOverlay> RAINBOW_FLAG = create("rainbow_flag");
|
||||
public static final ResourceKey<TurtleOverlay> TRANS_FLAG = create("trans_flag");
|
||||
public static final ResourceLocation RAINBOW_FLAG = create("rainbow_flag");
|
||||
public static final ResourceLocation TRANS_FLAG = create("trans_flag");
|
||||
|
||||
private static ResourceKey<TurtleOverlay> create(String name) {
|
||||
return ResourceKey.create(TurtleOverlay.REGISTRY, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name));
|
||||
private static ResourceLocation create(String name) {
|
||||
return ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
|
||||
}
|
||||
|
||||
private TurtleOverlays() {
|
||||
}
|
||||
|
||||
public static void register(BootstrapContext<TurtleOverlay> registry) {
|
||||
registry.register(RAINBOW_FLAG, new TurtleOverlay(
|
||||
public static void register(BiConsumer<ResourceLocation, TurtleOverlay.Unbaked> registry) {
|
||||
registry.accept(RAINBOW_FLAG, new TurtleOverlay.Unbaked(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_rainbow_overlay"),
|
||||
true
|
||||
));
|
||||
|
||||
registry.register(TRANS_FLAG, new TurtleOverlay(
|
||||
registry.accept(TRANS_FLAG, new TurtleOverlay.Unbaked(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_trans_overlay"),
|
||||
true
|
||||
));
|
||||
|
@@ -6,6 +6,10 @@ package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.api.client.turtle.BasicUpgradeModel;
|
||||
import dan200.computercraft.api.client.turtle.ItemUpgradeModel;
|
||||
import dan200.computercraft.api.client.turtle.SelectUpgradeModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable;
|
||||
@@ -17,21 +21,21 @@ import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static dan200.computercraft.api.turtle.TurtleToolBuilder.tool;
|
||||
|
||||
class TurtleUpgradeProvider {
|
||||
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
|
||||
upgrades.register(id("speaker"), new TurtleSpeaker(new ItemStack(ModRegistry.Items.SPEAKER.get())));
|
||||
upgrades.register(vanilla("crafting_table"), new TurtleCraftingTable(new ItemStack(Items.CRAFTING_TABLE)));
|
||||
upgrades.register(id("wireless_modem_normal"), new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), false));
|
||||
upgrades.register(id("wireless_modem_advanced"), new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get()), true));
|
||||
private static final ResourceKey<ITurtleUpgrade> SPEAKER = id("speaker");
|
||||
private static final ResourceKey<ITurtleUpgrade> CRAFTING_TABLE = vanilla("crafting_table");
|
||||
private static final ResourceKey<ITurtleUpgrade> WIRELESS_MODEM_NORMAL = id("wireless_modem_normal");
|
||||
private static final ResourceKey<ITurtleUpgrade> WIRELESS_MODEM_ADVANCED = id("wireless_modem_advanced");
|
||||
|
||||
tool(vanilla("diamond_axe").location(), Items.DIAMOND_AXE).damageMultiplier(6.0f).register(upgrades);
|
||||
tool(vanilla("diamond_pickaxe"), Items.DIAMOND_PICKAXE).register(upgrades);
|
||||
tool(vanilla("diamond_hoe"), Items.DIAMOND_HOE).breakable(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).register(upgrades);
|
||||
tool(vanilla("diamond_shovel"), Items.DIAMOND_SHOVEL).breakable(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).register(upgrades);
|
||||
tool(vanilla("diamond_sword"), Items.DIAMOND_SWORD).breakable(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).register(upgrades);
|
||||
}
|
||||
private static final ResourceKey<ITurtleUpgrade> DIAMOND_AXE = vanilla("diamond_axe");
|
||||
private static final ResourceKey<ITurtleUpgrade> DIAMOND_PICKAXE = vanilla("diamond_pickaxe");
|
||||
private static final ResourceKey<ITurtleUpgrade> DIAMOND_HOE = vanilla("diamond_hoe");
|
||||
private static final ResourceKey<ITurtleUpgrade> DIAMOND_SHOVEL = vanilla("diamond_shovel");
|
||||
private static final ResourceKey<ITurtleUpgrade> DIAMOND_SWORD = vanilla("diamond_sword");
|
||||
|
||||
private static ResourceKey<ITurtleUpgrade> id(String id) {
|
||||
return ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, id));
|
||||
@@ -41,4 +45,52 @@ class TurtleUpgradeProvider {
|
||||
// Naughty, please don't do this. Mostly here for some semblance of backwards compatibility.
|
||||
return ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath("minecraft", id));
|
||||
}
|
||||
|
||||
public static void register(BootstrapContext<ITurtleUpgrade> upgrades) {
|
||||
upgrades.register(SPEAKER, new TurtleSpeaker(new ItemStack(ModRegistry.Items.SPEAKER.get())));
|
||||
upgrades.register(CRAFTING_TABLE, new TurtleCraftingTable(new ItemStack(Items.CRAFTING_TABLE)));
|
||||
upgrades.register(WIRELESS_MODEM_NORMAL, new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), false));
|
||||
upgrades.register(WIRELESS_MODEM_ADVANCED, new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get()), true));
|
||||
|
||||
tool(DIAMOND_AXE, Items.DIAMOND_AXE).damageMultiplier(6.0f).register(upgrades);
|
||||
tool(DIAMOND_PICKAXE, Items.DIAMOND_PICKAXE).register(upgrades);
|
||||
tool(DIAMOND_HOE, Items.DIAMOND_HOE).breakable(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).register(upgrades);
|
||||
tool(DIAMOND_SHOVEL, Items.DIAMOND_SHOVEL).breakable(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).register(upgrades);
|
||||
tool(DIAMOND_SWORD, Items.DIAMOND_SWORD).breakable(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).register(upgrades);
|
||||
}
|
||||
|
||||
public static void addModels(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> out) {
|
||||
out.accept(SPEAKER.location(), BasicUpgradeModel.unbaked(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
|
||||
));
|
||||
out.accept(CRAFTING_TABLE.location(), BasicUpgradeModel.unbaked(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
|
||||
));
|
||||
|
||||
out.accept(WIRELESS_MODEM_NORMAL.location(), createModemModel("normal"));
|
||||
out.accept(WIRELESS_MODEM_ADVANCED.location(), createModemModel("advanced"));
|
||||
|
||||
out.accept(DIAMOND_AXE.location(), ItemUpgradeModel.unbaked());
|
||||
out.accept(DIAMOND_PICKAXE.location(), ItemUpgradeModel.unbaked());
|
||||
out.accept(DIAMOND_HOE.location(), ItemUpgradeModel.unbaked());
|
||||
out.accept(DIAMOND_SHOVEL.location(), ItemUpgradeModel.unbaked());
|
||||
out.accept(DIAMOND_SWORD.location(), ItemUpgradeModel.unbaked());
|
||||
}
|
||||
|
||||
private static TurtleUpgradeModel.Unbaked createModemModel(String type) {
|
||||
return SelectUpgradeModel.onComponent(ModRegistry.DataComponents.ON.get())
|
||||
.when(false, createBaseModemModel(type, "off"))
|
||||
.when(true, createBaseModemModel(type, "on"))
|
||||
.fallback(createBaseModemModel(type, "off"))
|
||||
.create();
|
||||
}
|
||||
|
||||
private static TurtleUpgradeModel.Unbaked createBaseModemModel(String type, String state) {
|
||||
return BasicUpgradeModel.unbaked(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_" + state + "_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_" + state + "_right")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -4,12 +4,14 @@
|
||||
|
||||
package dan200.computercraft.data.client;
|
||||
|
||||
import com.mojang.math.Quadrant;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.item.model.TurtleOverlayModel;
|
||||
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
|
||||
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
|
||||
@@ -20,12 +22,17 @@ import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.client.color.item.Dye;
|
||||
import net.minecraft.client.data.models.BlockModelGenerators;
|
||||
import net.minecraft.client.data.models.blockstates.*;
|
||||
import net.minecraft.client.data.models.MultiVariant;
|
||||
import net.minecraft.client.data.models.blockstates.ConditionBuilder;
|
||||
import net.minecraft.client.data.models.blockstates.MultiPartGenerator;
|
||||
import net.minecraft.client.data.models.blockstates.MultiVariantGenerator;
|
||||
import net.minecraft.client.data.models.blockstates.PropertyDispatch;
|
||||
import net.minecraft.client.data.models.model.*;
|
||||
import net.minecraft.client.renderer.block.model.VariantMutator;
|
||||
import net.minecraft.client.renderer.item.EmptyModel;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.HasComponent;
|
||||
import net.minecraft.core.Direction;
|
||||
@@ -33,7 +40,6 @@ import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
@@ -43,6 +49,7 @@ import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static net.minecraft.client.data.models.BlockModelGenerators.*;
|
||||
import static net.minecraft.client.data.models.model.ModelLocationUtils.getModelLocation;
|
||||
import static net.minecraft.client.data.models.model.TextureMapping.getBlockTexture;
|
||||
|
||||
@@ -116,15 +123,14 @@ public class BlockModelProvider {
|
||||
registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
|
||||
|
||||
generators.blockStateOutput.accept(
|
||||
BlockModelGenerators.createSimpleBlock(ModRegistry.Blocks.LECTERN.get(), getModelLocation(Blocks.LECTERN))
|
||||
.with(createHorizontalFacingDispatch())
|
||||
createSimpleBlock(ModRegistry.Blocks.LECTERN.get(), plainVariant(getModelLocation(Blocks.LECTERN)))
|
||||
.with(ROTATION_HORIZONTAL_FACING)
|
||||
);
|
||||
}
|
||||
|
||||
private static void registerDiskDrive(BlockModelGenerators generators) {
|
||||
var diskDrive = ModRegistry.Blocks.DISK_DRIVE.get();
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(diskDrive)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(diskDrive)
|
||||
.with(createModelDispatch(DiskDriveBlock.STATE, value -> {
|
||||
var textureSuffix = switch (value) {
|
||||
case EMPTY -> "_front";
|
||||
@@ -137,14 +143,14 @@ public class BlockModelProvider {
|
||||
generators.modelOutput
|
||||
);
|
||||
}))
|
||||
.with(ROTATION_HORIZONTAL_FACING)
|
||||
);
|
||||
generators.registerSimpleItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
|
||||
}
|
||||
|
||||
private static void registerPrinter(BlockModelGenerators generators) {
|
||||
var printer = ModRegistry.Blocks.PRINTER.get();
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(printer)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(printer)
|
||||
.with(createModelDispatch(PrinterBlock.TOP, PrinterBlock.BOTTOM, (top, bottom) -> {
|
||||
String model, texture;
|
||||
if (top && bottom) {
|
||||
@@ -165,13 +171,13 @@ public class BlockModelProvider {
|
||||
generators.modelOutput
|
||||
);
|
||||
}))
|
||||
.with(ROTATION_HORIZONTAL_FACING)
|
||||
);
|
||||
generators.registerSimpleItemModel(printer, getModelLocation(printer, "_empty"));
|
||||
}
|
||||
|
||||
private static void registerComputer(BlockModelGenerators generators, ComputerBlock<?> block) {
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
|
||||
.with(createModelDispatch(ComputerBlock.STATE, state -> switch (state) {
|
||||
case OFF -> ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
|
||||
block, "_" + state.getSerializedName(),
|
||||
@@ -184,6 +190,7 @@ public class BlockModelProvider {
|
||||
generators.modelOutput
|
||||
);
|
||||
}))
|
||||
.with(ROTATION_HORIZONTAL_FACING)
|
||||
);
|
||||
generators.registerSimpleItemModel(block, getModelLocation(block, "_blinking"));
|
||||
}
|
||||
@@ -193,7 +200,7 @@ public class BlockModelProvider {
|
||||
var particleModel = ModelTemplates.PARTICLE_ONLY.createWithSuffix(
|
||||
block, "_particle", TextureMapping.particle(getBlockTexture(block, "_front")), generators.modelOutput
|
||||
);
|
||||
generators.blockStateOutput.accept(BlockModelGenerators.createSimpleBlock(block, particleModel));
|
||||
generators.blockStateOutput.accept(createSimpleBlock(block, plainVariant(particleModel)));
|
||||
|
||||
// We then register the full model for use in items and the BE renderer.
|
||||
var model = TURTLE.create(block, new TextureMapping()
|
||||
@@ -210,7 +217,7 @@ public class BlockModelProvider {
|
||||
generators.itemModelOutput.accept(block.asItem(), ItemModelUtils.composite(
|
||||
ItemModelUtils.conditional(
|
||||
new HasComponent(DataComponents.DYED_COLOR, false),
|
||||
ItemModelUtils.plainModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL),
|
||||
ItemModelUtils.tintedModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL, new Dye(-1)),
|
||||
ItemModelUtils.plainModel(model)
|
||||
),
|
||||
new TurtleUpgradeModel.Unbaked(TurtleSide.LEFT, model),
|
||||
@@ -224,17 +231,17 @@ public class BlockModelProvider {
|
||||
}
|
||||
|
||||
private static void registerWirelessModem(BlockModelGenerators generators, WirelessModemBlock block) {
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
|
||||
.with(createFacingDispatch())
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
|
||||
.with(createModelDispatch(WirelessModemBlock.ON,
|
||||
on -> modemModel(generators, getModelLocation(block, on ? "_on" : "_off"), getBlockTexture(block, "_face" + (on ? "_on" : "")))
|
||||
)));
|
||||
))
|
||||
.with(ROTATION_FACING));
|
||||
generators.registerSimpleItemModel(block, getModelLocation(block, "_off"));
|
||||
}
|
||||
|
||||
private static void registerWiredModems(BlockModelGenerators generators) {
|
||||
var fullBlock = ModRegistry.Blocks.WIRED_MODEM_FULL.get();
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(fullBlock)
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(fullBlock)
|
||||
.with(createModelDispatch(WiredModemFullBlock.MODEM_ON, WiredModemFullBlock.PERIPHERAL_ON, (on, peripheral) -> {
|
||||
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
|
||||
var faceTexture = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_face" + (peripheral ? "_peripheral" : "") + (on ? "_on" : ""));
|
||||
@@ -281,10 +288,10 @@ public class BlockModelProvider {
|
||||
monitorModel(generators, block, "_u", 22, 5, 0, 38);
|
||||
monitorModel(generators, block, "_ud", 21, 6, 0, 37);
|
||||
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
|
||||
.with(createHorizontalFacingDispatch())
|
||||
.with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.dispatch(block)
|
||||
.with(createModelDispatch(MonitorBlock.STATE, edge -> getModelLocation(block, edge == MonitorEdgeState.NONE ? "" : "_" + edge.getSerializedName())))
|
||||
.with(ROTATION_HORIZONTAL_FACING)
|
||||
.with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
|
||||
);
|
||||
generators.registerSimpleItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
|
||||
}
|
||||
@@ -308,55 +315,54 @@ public class BlockModelProvider {
|
||||
var coreFacing = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_core_facing");
|
||||
// Up/Down
|
||||
generator.with(
|
||||
Condition.or(
|
||||
or(
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.UP, true),
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST).term(CableBlock.DOWN, true)
|
||||
),
|
||||
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.X_ROT, VariantProperties.Rotation.R90)
|
||||
plainVariant(coreFacing).with(VariantMutator.X_ROT.withValue(Quadrant.R90))
|
||||
);
|
||||
|
||||
// North/South and no neighbours
|
||||
generator.with(
|
||||
Condition.or(
|
||||
or(
|
||||
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST),
|
||||
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.NORTH, true),
|
||||
cableNoNeighbour(Direction.UP, Direction.DOWN, Direction.EAST, Direction.WEST).term(CableBlock.SOUTH, true)
|
||||
),
|
||||
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R0)
|
||||
plainVariant(coreFacing).with(VariantMutator.Y_ROT.withValue(Quadrant.R0))
|
||||
);
|
||||
|
||||
// East/West
|
||||
generator.with(
|
||||
Condition.or(
|
||||
or(
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.EAST, true),
|
||||
cableNoNeighbour(Direction.NORTH, Direction.SOUTH, Direction.UP, Direction.DOWN).term(CableBlock.WEST, true)
|
||||
),
|
||||
Variant.variant().with(VariantProperties.MODEL, coreFacing).with(VariantProperties.Y_ROT, VariantProperties.Rotation.R90)
|
||||
plainVariant(coreFacing).with(VariantMutator.Y_ROT.withValue(Quadrant.R90))
|
||||
);
|
||||
|
||||
// Find all other possibilities and emit a "solid" core which doesn't have a facing direction.
|
||||
var core = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_core_any");
|
||||
List<Condition.TerminalCondition> rightAngles = new ArrayList<>();
|
||||
List<ConditionBuilder> rightAngles = new ArrayList<>();
|
||||
for (var i = 0; i < DirectionUtil.FACINGS.length; i++) {
|
||||
for (var j = i; j < DirectionUtil.FACINGS.length; j++) {
|
||||
if (DirectionUtil.FACINGS[i].getAxis() == DirectionUtil.FACINGS[j].getAxis()) continue;
|
||||
|
||||
rightAngles.add(new Condition.TerminalCondition()
|
||||
rightAngles.add(condition()
|
||||
.term(CableBlock.CABLE, true).term(CABLE_DIRECTIONS[i], true).term(CABLE_DIRECTIONS[j], true)
|
||||
);
|
||||
}
|
||||
}
|
||||
generator.with(Condition.or(rightAngles.toArray(new Condition[0])), Variant.variant().with(VariantProperties.MODEL, core));
|
||||
generator.with(or(rightAngles.toArray(new ConditionBuilder[0])), plainVariant(core));
|
||||
|
||||
// Then emit the actual cable arms
|
||||
var arm = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/cable_arm");
|
||||
for (var direction : DirectionUtil.FACINGS) {
|
||||
generator.with(
|
||||
new Condition.TerminalCondition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
|
||||
Variant.variant()
|
||||
.with(VariantProperties.MODEL, arm)
|
||||
.with(VariantProperties.X_ROT, toXAngle(direction.getOpposite()))
|
||||
.with(VariantProperties.Y_ROT, toYAngle(direction.getOpposite()))
|
||||
condition().term(CABLE_DIRECTIONS[direction.ordinal()], true),
|
||||
plainVariant(arm)
|
||||
.with(VariantMutator.X_ROT.withValue(toXAngle(direction.getOpposite())))
|
||||
.with(VariantMutator.Y_ROT.withValue(toYAngle(direction.getOpposite())))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -366,11 +372,10 @@ public class BlockModelProvider {
|
||||
for (var peripheral : BOOLEANS) {
|
||||
var suffix = (on ? "_on" : "_off") + (peripheral ? "_peripheral" : "");
|
||||
generator.with(
|
||||
new Condition.TerminalCondition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
|
||||
Variant.variant()
|
||||
.with(VariantProperties.MODEL, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
|
||||
.with(VariantProperties.X_ROT, toXAngle(direction))
|
||||
.with(VariantProperties.Y_ROT, toYAngle(direction))
|
||||
condition().term(CableBlock.MODEM, CableModemVariant.from(direction, on, peripheral)),
|
||||
plainVariant(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem" + suffix))
|
||||
.with(VariantMutator.X_ROT.withValue(toXAngle(direction)))
|
||||
.with(VariantMutator.Y_ROT.withValue(toYAngle(direction)))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -390,8 +395,8 @@ public class BlockModelProvider {
|
||||
private static final BooleanProperty[] CABLE_DIRECTIONS = { CableBlock.DOWN, CableBlock.UP, CableBlock.NORTH, CableBlock.SOUTH, CableBlock.WEST, CableBlock.EAST };
|
||||
private static final boolean[] BOOLEANS = new boolean[]{ false, true };
|
||||
|
||||
private static Condition.TerminalCondition cableNoNeighbour(Direction... directions) {
|
||||
var condition = new Condition.TerminalCondition().term(CableBlock.CABLE, true);
|
||||
private static ConditionBuilder cableNoNeighbour(Direction... directions) {
|
||||
var condition = condition().term(CableBlock.CABLE, true);
|
||||
for (var direction : directions) condition.term(CABLE_DIRECTIONS[direction.ordinal()], false);
|
||||
return condition;
|
||||
}
|
||||
@@ -418,66 +423,47 @@ public class BlockModelProvider {
|
||||
generators.registerSimpleItemModel(block, ModelLocationUtils.getModelLocation(block));
|
||||
}
|
||||
|
||||
private static VariantProperties.Rotation toXAngle(Direction direction) {
|
||||
private static Quadrant toXAngle(Direction direction) {
|
||||
return switch (direction) {
|
||||
default -> VariantProperties.Rotation.R0;
|
||||
case UP -> VariantProperties.Rotation.R270;
|
||||
case DOWN -> VariantProperties.Rotation.R90;
|
||||
default -> Quadrant.R0;
|
||||
case UP -> Quadrant.R270;
|
||||
case DOWN -> Quadrant.R90;
|
||||
};
|
||||
}
|
||||
|
||||
private static VariantProperties.Rotation toYAngle(Direction direction) {
|
||||
private static Quadrant toYAngle(Direction direction) {
|
||||
return switch (direction) {
|
||||
default -> VariantProperties.Rotation.R0;
|
||||
case NORTH -> VariantProperties.Rotation.R0;
|
||||
case SOUTH -> VariantProperties.Rotation.R180;
|
||||
case EAST -> VariantProperties.Rotation.R90;
|
||||
case WEST -> VariantProperties.Rotation.R270;
|
||||
default -> Quadrant.R0;
|
||||
case NORTH -> Quadrant.R0;
|
||||
case SOUTH -> Quadrant.R180;
|
||||
case EAST -> Quadrant.R90;
|
||||
case WEST -> Quadrant.R270;
|
||||
};
|
||||
}
|
||||
|
||||
private static PropertyDispatch createHorizontalFacingDispatch() {
|
||||
var dispatch = PropertyDispatch.property(BlockStateProperties.HORIZONTAL_FACING);
|
||||
for (var direction : BlockStateProperties.HORIZONTAL_FACING.getPossibleValues()) {
|
||||
dispatch.select(direction, Variant.variant().with(VariantProperties.Y_ROT, toYAngle(direction)));
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
private static PropertyDispatch createVerticalFacingDispatch(Property<Direction> property) {
|
||||
var dispatch = PropertyDispatch.property(property);
|
||||
private static PropertyDispatch<VariantMutator> createVerticalFacingDispatch(Property<Direction> property) {
|
||||
var dispatch = PropertyDispatch.modify(property);
|
||||
for (var direction : property.getPossibleValues()) {
|
||||
dispatch.select(direction, Variant.variant().with(VariantProperties.X_ROT, toXAngle(direction)));
|
||||
dispatch.select(direction, VariantMutator.X_ROT.withValue(toXAngle(direction)));
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
private static PropertyDispatch createFacingDispatch() {
|
||||
var dispatch = PropertyDispatch.property(BlockStateProperties.FACING);
|
||||
for (var direction : BlockStateProperties.FACING.getPossibleValues()) {
|
||||
dispatch.select(direction, Variant.variant()
|
||||
.with(VariantProperties.Y_ROT, toYAngle(direction))
|
||||
.with(VariantProperties.X_ROT, toXAngle(direction))
|
||||
);
|
||||
}
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> PropertyDispatch createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
|
||||
var variant = PropertyDispatch.property(property);
|
||||
private static <T extends Comparable<T>> PropertyDispatch<MultiVariant> createModelDispatch(Property<T> property, Function<T, ResourceLocation> makeModel) {
|
||||
var variant = PropertyDispatch.initial(property);
|
||||
for (var value : property.getPossibleValues()) {
|
||||
variant.select(value, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(value)));
|
||||
variant.select(value, plainVariant(makeModel.apply(value)));
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch createModelDispatch(
|
||||
private static <T extends Comparable<T>, U extends Comparable<U>> PropertyDispatch<MultiVariant> createModelDispatch(
|
||||
Property<T> propertyT, Property<U> propertyU, BiFunction<T, U, ResourceLocation> makeModel
|
||||
) {
|
||||
var variant = PropertyDispatch.properties(propertyT, propertyU);
|
||||
var variant = PropertyDispatch.initial(propertyT, propertyU);
|
||||
for (var valueT : propertyT.getPossibleValues()) {
|
||||
for (var valueU : propertyU.getPossibleValues()) {
|
||||
variant.select(valueT, valueU, Variant.variant().with(VariantProperties.MODEL, makeModel.apply(valueT, valueU)));
|
||||
variant.select(valueT, valueU, plainVariant(makeModel.apply(valueT, valueU)));
|
||||
}
|
||||
}
|
||||
return variant;
|
||||
|
@@ -1,52 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.data.client;
|
||||
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import dan200.computercraft.client.model.ExtraModels;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.data.CachedOutput;
|
||||
import net.minecraft.data.DataProvider;
|
||||
import net.minecraft.data.PackOutput;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A data provider to generate {@link ExtraModels}.
|
||||
*/
|
||||
public abstract class ExtraModelsProvider implements DataProvider {
|
||||
private final Path path;
|
||||
private final CompletableFuture<HolderLookup.Provider> registries;
|
||||
|
||||
public ExtraModelsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
|
||||
path = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(ExtraModels.PATH.getNamespace()).resolve(ExtraModels.PATH.getPath());
|
||||
this.registries = registries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stream of models to load.
|
||||
*
|
||||
* @param registries The current registries.
|
||||
* @return The collection of extra models to load.
|
||||
*/
|
||||
public abstract Stream<ResourceLocation> getModels(HolderLookup.Provider registries);
|
||||
|
||||
@Override
|
||||
public final CompletableFuture<?> run(CachedOutput output) {
|
||||
return registries.thenCompose(registries -> {
|
||||
var models = new ExtraModels(getModels(registries).sorted().toList());
|
||||
var json = ExtraModels.CODEC.encodeStart(JsonOps.INSTANCE, models).getOrThrow(IllegalStateException::new);
|
||||
return DataProvider.saveStable(output, json, path);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getName() {
|
||||
return "Extra Models";
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "computercraft:item"
|
||||
}
|
@@ -11,7 +11,7 @@ import net.minecraft.world.item.ItemStack;
|
||||
// @start region=body
|
||||
public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade {
|
||||
public ExampleTurtleUpgrade(ItemStack stack) {
|
||||
super(TurtleUpgradeType.PERIPHERAL, "example", stack);
|
||||
super(TurtleUpgradeType.PERIPHERAL, "upgrade.examplemod.example_turtle_upgrade.adjective", stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -2,6 +2,8 @@ package com.example.examplemod.data;
|
||||
|
||||
import com.example.examplemod.ExampleMod;
|
||||
import com.example.examplemod.ExampleTurtleUpgrade;
|
||||
import dan200.computercraft.api.client.turtle.ItemUpgradeModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
@@ -13,18 +15,19 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Extends the bootstrap registries with our {@linkplain ExampleTurtleUpgrade example turtle upgrade}.
|
||||
*/
|
||||
// @start region=body
|
||||
public class TurtleUpgradeProvider {
|
||||
// Define our upgrade ids.
|
||||
private static final ResourceLocation EXAMPLE_TURTLE_UPGRADE = ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade");
|
||||
|
||||
// Register our turtle upgrades.
|
||||
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
|
||||
upgrades.register(
|
||||
ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade")),
|
||||
new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS))
|
||||
);
|
||||
upgrades.register(ITurtleUpgrade.createKey(EXAMPLE_TURTLE_UPGRADE), new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS)));
|
||||
}
|
||||
|
||||
// Set up the dynamic registries to contain our turtle upgrades.
|
||||
@@ -33,5 +36,10 @@ public class TurtleUpgradeProvider {
|
||||
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
|
||||
}));
|
||||
}
|
||||
|
||||
// Register our turtle models.
|
||||
public static void addUpgradeModels(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> models) {
|
||||
models.accept(EXAMPLE_TURTLE_UPGRADE, ItemUpgradeModel.unbaked());
|
||||
}
|
||||
}
|
||||
// @end region=body
|
||||
|
@@ -28,7 +28,7 @@ public class BrewingStandPeripheral implements IPeripheral {
|
||||
@LuaFunction
|
||||
public final int getFuel() {
|
||||
// Don't do it this way! Use an access widener/transformer to access the "fuel" field instead.
|
||||
return brewingStand.saveWithoutMetadata(brewingStand.getLevel().registryAccess()).getInt("Fuel");
|
||||
return brewingStand.saveWithoutMetadata(brewingStand.getLevel().registryAccess()).getByteOr("Fuel", (byte) 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -23,7 +23,7 @@ public class FurnacePeripheral implements GenericPeripheral {
|
||||
@LuaFunction(mainThread = true)
|
||||
public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
|
||||
// Don't do it this way! Use an access widener/transformer to access the "litTime" field instead.
|
||||
return furnace.saveWithoutMetadata(furnace.getLevel().registryAccess()).getInt("BurnTime");
|
||||
return furnace.saveWithoutMetadata(furnace.getLevel().registryAccess()).getShortOr("lit_time_remaining", (short) 0);
|
||||
}
|
||||
}
|
||||
// @end region=body
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user