mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-15 14:07:38 +00:00
Compare commits
87 Commits
v1.21.1-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 | ||
![]() |
9277aa33e9 | ||
![]() |
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
|
||||
|
@@ -4,10 +4,7 @@
|
||||
|
||||
/** Default configuration for Fabric projects. */
|
||||
|
||||
import cc.tweaked.gradle.CCTweakedExtension
|
||||
import cc.tweaked.gradle.CCTweakedPlugin
|
||||
import cc.tweaked.gradle.IdeaRunConfigurations
|
||||
import cc.tweaked.gradle.MinecraftConfigurations
|
||||
import cc.tweaked.gradle.*
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
@@ -67,3 +64,9 @@ dependencies {
|
||||
tasks.ideaSyncTask {
|
||||
doLast { IdeaRunConfigurations(project).patch() }
|
||||
}
|
||||
|
||||
tasks.named("checkDependencyConsistency", DependencyCheck::class.java) {
|
||||
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
// Minecraft depends on asm, but Fabric forces it to a more recent version
|
||||
override(libs.findLibrary("asm").get(), "9.8")
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -129,7 +129,7 @@ SPDX-License-Identifier: MPL-2.0
|
||||
<module name="LocalFinalVariableName" />
|
||||
<module name="LocalVariableName" />
|
||||
<module name="MemberName">
|
||||
<property name="format" value="^\$?[a-z][a-zA-Z0-9]*$" />
|
||||
<property name="format" value="^(computercraft\$|\$)?[a-z][a-zA-Z0-9]*$" />
|
||||
</module>
|
||||
<module name="MethodName">
|
||||
<property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
|
||||
|
@@ -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.1
|
||||
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.102.1+1.21.1"
|
||||
fabric-loader = "0.15.11"
|
||||
neoForge = "21.1.9"
|
||||
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.07.28"
|
||||
parchmentMc = "1.21"
|
||||
yarn = "1.21.1+build.1"
|
||||
parchment = "2025.06.29"
|
||||
parchmentMc = "1.21.6"
|
||||
yarn = "1.21.7+build.1"
|
||||
|
||||
# Core dependencies (these versions are tied to the version Minecraft uses)
|
||||
fastutil = "8.5.12"
|
||||
guava = "32.1.2-jre"
|
||||
netty = "4.1.97.Final"
|
||||
slf4j = "2.0.9"
|
||||
fastutil = "8.5.15"
|
||||
guava = "33.3.1-jre"
|
||||
netty = "4.1.118.Final"
|
||||
slf4j = "2.0.16"
|
||||
|
||||
# Core dependencies (independent of Minecraft)
|
||||
asm = "9.6"
|
||||
asm = "9.7.1"
|
||||
autoService = "1.1.1"
|
||||
checkerFramework = "3.42.0"
|
||||
cobalt = { strictly = "0.9.5" }
|
||||
cobalt = { strictly = "0.9.6" }
|
||||
commonsCli = "1.6.0"
|
||||
jetbrainsAnnotations = "24.1.0"
|
||||
jspecify = "1.0.0"
|
||||
@@ -37,15 +37,15 @@ nightConfig = "3.8.1"
|
||||
|
||||
# Minecraft mods
|
||||
emi = "1.1.7+1.21"
|
||||
fabricPermissions = "0.3.1"
|
||||
iris-fabric = "1.8.0-beta.3+1.21-fabric"
|
||||
iris-forge = "1.8.0-beta.3+1.21-neoforge"
|
||||
jei = "19.8.2.99"
|
||||
modmenu = "11.0.0-rc.4"
|
||||
fabricPermissions = "0.3.3"
|
||||
iris-fabric = "1.9.1+1.21.7-fabric"
|
||||
iris-forge = "1.9.1+1.21.7-neoforge"
|
||||
jei = "23.1.0.4"
|
||||
modmenu = "13.0.2"
|
||||
moreRed = "6.0.0.3"
|
||||
rei = "16.0.729"
|
||||
sodium-fabric = "mc1.21-0.6.0-beta.1-fabric"
|
||||
sodium-forge = "mc1.21-0.6.0-beta.1-neoforge"
|
||||
rei = "18.0.800"
|
||||
sodium-fabric = "mc1.21.6-0.6.13-fabric"
|
||||
sodium-forge = "mc1.21.6-0.6.13-neoforge"
|
||||
mixinExtra = "0.3.5"
|
||||
create-forge = "6.0.0-6"
|
||||
create-fabric = "0.5.1-f-build.1467+mc1.20.1"
|
||||
@@ -58,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" }
|
||||
@@ -188,7 +188,7 @@ externalMods-common = ["iris-forge", "jei-api", "nightConfig-core", "nightConfig
|
||||
externalMods-forge-compile = ["moreRed", "iris-forge", "jei-api"]
|
||||
externalMods-forge-runtime = ["jei-forge"]
|
||||
externalMods-fabric-compile = ["fabricPermissions", "iris-fabric", "jei-api", "rei-api", "rei-builtin"]
|
||||
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
|
||||
externalMods-fabric-runtime = []
|
||||
|
||||
# Testing
|
||||
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
|
||||
|
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",
|
||||
|
@@ -1,84 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client;
|
||||
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The location of a model to load. This may either be:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A {@link ModelResourceLocation}, referencing an already baked model (such as {@code minecraft:dirt#inventory}).</li>
|
||||
* <li>
|
||||
* A {@link ResourceLocation}, referencing a path to a model resource (such as {@code minecraft:item/dirt}.
|
||||
* These models will be baked and stored in the {@link ModelManager} in a loader-specific way.
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class ModelLocation {
|
||||
/**
|
||||
* The location of the model.
|
||||
* <p>
|
||||
* When {@link #resourceLocation} is null, this is the location of the model to load. When {@link #resourceLocation}
|
||||
* is non-null, this is the "standalone" variant of the model resource — this is used by NeoForge's implementation
|
||||
* of {@link ClientPlatformHelper#getModel(ModelManager, ModelResourceLocation, ResourceLocation)} to fetch the
|
||||
* model from the model manger. It is not used on Fabric.
|
||||
*/
|
||||
private final ModelResourceLocation modelLocation;
|
||||
private final @Nullable ResourceLocation resourceLocation;
|
||||
|
||||
private ModelLocation(ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation) {
|
||||
this.modelLocation = modelLocation;
|
||||
this.resourceLocation = resourceLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link ModelLocation} from model in the model manager.
|
||||
*
|
||||
* @param location The name of the model to load.
|
||||
* @return The new {@link ModelLocation} instance.
|
||||
*/
|
||||
public static ModelLocation ofModel(ModelResourceLocation location) {
|
||||
return new ModelLocation(location, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link ModelLocation} from a resource.
|
||||
*
|
||||
* @param location The location of the model resource, such as {@code minecraft:item/dirt}.
|
||||
* @return The new {@link ModelLocation} instance.
|
||||
*/
|
||||
public static ModelLocation ofResource(ResourceLocation location) {
|
||||
return new ModelLocation(new ModelResourceLocation(location, "standalone"), location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this model from the provided model manager.
|
||||
*
|
||||
* @param manager The model manger.
|
||||
* @return This model, or the missing model if it could not be found.
|
||||
*/
|
||||
public BakedModel getModel(ModelManager manager) {
|
||||
return ClientPlatformHelper.get().getModel(manager, modelLocation, resourceLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the models this model location depends on.
|
||||
*
|
||||
* @return A list of models that this model location depends on.
|
||||
* @see TurtleUpgradeModeller#getDependencies()
|
||||
*/
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return resourceLocation == null ? Stream.empty() : Stream.of(resourceLocation);
|
||||
}
|
||||
}
|
@@ -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,66 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
|
||||
package dan200.computercraft.api.client;
|
||||
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* A model to render, combined with a transformation matrix to apply.
|
||||
*
|
||||
* @param model The model.
|
||||
* @param matrix The transformation matrix.
|
||||
*/
|
||||
public record TransformedModel(BakedModel model, Transformation matrix) {
|
||||
public TransformedModel(BakedModel model) {
|
||||
this(model, Transformation.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
|
||||
*
|
||||
* @param location The location of the model to load.
|
||||
* @return The new {@link TransformedModel} instance.
|
||||
*/
|
||||
public static TransformedModel of(ModelLocation location) {
|
||||
var modelManager = Minecraft.getInstance().getModelManager();
|
||||
return new TransformedModel(location.getModel(modelManager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
|
||||
*
|
||||
* @param location The location of the model to load.
|
||||
* @return The new {@link TransformedModel} instance.
|
||||
* @see ModelLocation#ofModel(ModelResourceLocation)
|
||||
*/
|
||||
public static TransformedModel of(ModelResourceLocation location) {
|
||||
var modelManager = Minecraft.getInstance().getModelManager();
|
||||
return new TransformedModel(modelManager.getModel(location));
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
|
||||
*
|
||||
* @param location The location of the model to load.
|
||||
* @return The new {@link TransformedModel} instance.
|
||||
* @see ModelLocation#ofResource(ResourceLocation)
|
||||
*/
|
||||
public static TransformedModel of(ResourceLocation location) {
|
||||
var modelManager = Minecraft.getInstance().getModelManager();
|
||||
return new TransformedModel(ClientPlatformHelper.get().getModel(modelManager, location));
|
||||
}
|
||||
|
||||
public static TransformedModel of(ItemStack item, Transformation transform) {
|
||||
var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(item);
|
||||
return new TransformedModel(model, transform);
|
||||
}
|
||||
}
|
@@ -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,112 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.client.ModelLocation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import net.minecraft.client.resources.model.UnbakedModel;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Provides models for a {@link ITurtleUpgrade}.
|
||||
* <p>
|
||||
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
|
||||
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
|
||||
* on Forge.
|
||||
*
|
||||
* <h2>Example</h2>
|
||||
* <h3>Fabric</h3>
|
||||
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
|
||||
*
|
||||
* <h3>Forge</h3>
|
||||
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
|
||||
*
|
||||
* @param <T> The type of turtle upgrade this modeller applies to.
|
||||
* @see RegisterTurtleUpgradeModeller For multi-loader registration support.
|
||||
*/
|
||||
public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
|
||||
/**
|
||||
* Obtain the model to be used when rendering a turtle peripheral.
|
||||
* <p>
|
||||
* When the current turtle is {@literal null}, this function should be constant for a given upgrade, side and data.
|
||||
*
|
||||
* @param upgrade The upgrade that you're getting the model for.
|
||||
* @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models.
|
||||
* @param side Which side of the turtle (left or right) the upgrade resides on.
|
||||
* @param data Upgrade data instance for current turtle side.
|
||||
* @return The model that you wish to be used to render your upgrade.
|
||||
*/
|
||||
TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data);
|
||||
|
||||
/**
|
||||
* Get the models that this turtle modeller depends on.
|
||||
* <p>
|
||||
* Models included in this stream will be loaded and baked alongside item and block models, and so may be referenced
|
||||
* by {@link TransformedModel#of(ResourceLocation)}. You do not need to override this method if you will load models
|
||||
* by other means.
|
||||
*
|
||||
* @return A list of models that this modeller depends on.
|
||||
* @see UnbakedModel#getDependencies()
|
||||
*/
|
||||
default Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)}
|
||||
* upgrade item}.
|
||||
* <p>
|
||||
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
|
||||
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
|
||||
*
|
||||
* @param <T> The type of the turtle upgrade.
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> flatItem() {
|
||||
return (TurtleUpgradeModeller<T>) TurtleUpgradeModellers.UPGRADE_ITEM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link TurtleUpgradeModeller} which has a single model for the left and right side.
|
||||
*
|
||||
* @param left The model to use on the left.
|
||||
* @param right The model to use on the right.
|
||||
* @param <T> The type of the turtle upgrade.
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ResourceLocation left, ResourceLocation right) {
|
||||
return sided(ModelLocation.ofResource(left), ModelLocation.ofResource(right));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link TurtleUpgradeModeller} which has a single model for the left and right side.
|
||||
*
|
||||
* @param left The model to use on the left.
|
||||
* @param right The model to use on the right.
|
||||
* @param <T> The type of the turtle upgrade.
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ModelLocation left, ModelLocation right) {
|
||||
return new TurtleUpgradeModeller<>() {
|
||||
@Override
|
||||
public TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
|
||||
return TransformedModel.of(side == TurtleSide.LEFT ? left : right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of(left, right).flatMap(ModelLocation::getDependencies);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import org.joml.Matrix4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
final class TurtleUpgradeModellers {
|
||||
private static final Transformation leftTransform = getMatrixFor(-0.4065f);
|
||||
private static final Transformation rightTransform = getMatrixFor(0.4065f);
|
||||
|
||||
private static Transformation getMatrixFor(float offset) {
|
||||
var matrix = new Matrix4f();
|
||||
matrix.set(new float[]{
|
||||
0.0f, 0.0f, -1.0f, 1.0f + offset,
|
||||
1.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, -1.0f, 0.0f, 1.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f,
|
||||
});
|
||||
matrix.transpose();
|
||||
return new Transformation(matrix);
|
||||
}
|
||||
|
||||
static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
|
||||
|
||||
private static final class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
|
||||
@Override
|
||||
public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
|
||||
var stack = upgrade.getUpgradeItem(data);
|
||||
var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(stack);
|
||||
if (stack.hasFoil()) model = ClientPlatformHelper.get().createdFoiledModel(model);
|
||||
return new TransformedModel(model, side == TurtleSide.LEFT ? leftTransform : rightTransform);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.impl.client;
|
||||
|
||||
import dan200.computercraft.api.client.ModelLocation;
|
||||
import dan200.computercraft.impl.Services;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public interface ClientPlatformHelper {
|
||||
/**
|
||||
* Get a model from a resource.
|
||||
*
|
||||
* @param manager The model manager.
|
||||
* @param resourceLocation The model resourceLocation.
|
||||
* @return The baked model.
|
||||
* @see ModelLocation
|
||||
*/
|
||||
BakedModel getModel(ModelManager manager, ResourceLocation resourceLocation);
|
||||
|
||||
/**
|
||||
* Set a model from a {@link ModelResourceLocation} or {@link ResourceLocation}.
|
||||
* <p>
|
||||
* This is largely equivalent to {@code resourceLocation == null ? manager.getModel(modelLocation) : getModel(manager, resourceLocation)},
|
||||
* but allows pre-computing {@code modelLocation} (if needed).
|
||||
*
|
||||
* @param manager The model manager.
|
||||
* @param modelLocation The location of the model to load.
|
||||
* @param resourceLocation The location of the resource, if trying to load from a resource.
|
||||
* @return The baked model.
|
||||
* @see ModelLocation
|
||||
*/
|
||||
BakedModel getModel(ModelManager manager, ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation);
|
||||
|
||||
/**
|
||||
* Wrap this model in a version which renders a foil/enchantment glint.
|
||||
*
|
||||
* @param model The model to wrap.
|
||||
* @return The wrapped model.
|
||||
* @see RenderType#glint()
|
||||
*/
|
||||
BakedModel createdFoiledModel(BakedModel model);
|
||||
|
||||
static ClientPlatformHelper get() {
|
||||
var instance = Instance.INSTANCE;
|
||||
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
|
||||
}
|
||||
|
||||
final class Instance {
|
||||
static final @Nullable ClientPlatformHelper INSTANCE;
|
||||
static final @Nullable Throwable ERROR;
|
||||
|
||||
static {
|
||||
var helper = Services.tryLoad(ClientPlatformHelper.class);
|
||||
INSTANCE = helper.instance();
|
||||
ERROR = helper.error();
|
||||
}
|
||||
|
||||
private Instance() {
|
||||
}
|
||||
}
|
||||
}
|
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,8 +11,6 @@ import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.lua.IComputerSystem;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.api.media.IMedia;
|
||||
import dan200.computercraft.api.media.MediaProvider;
|
||||
import dan200.computercraft.api.network.PacketNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
@@ -142,19 +140,6 @@ public final class ComputerCraftAPI {
|
||||
return getInstance().getBundledRedstoneOutput(world, pos, side);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a media provider to provide {@link IMedia} implementations for Items.
|
||||
*
|
||||
* @param provider The media provider to register.
|
||||
* @see MediaProvider
|
||||
* @deprecated Prefer {@code dan200.computercraft.api.media.MediaLookup} (Fabric) or
|
||||
* {@code dan200.computercraft.api.media.MediaCapability} (NeoForge).
|
||||
*/
|
||||
@Deprecated
|
||||
public static void registerMediaProvider(MediaProvider provider) {
|
||||
getInstance().registerMediaProvider(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get the game-wide wireless network.
|
||||
*
|
||||
|
@@ -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) {
|
||||
|
@@ -1,26 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. This API may be redistributed unmodified and in full only.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.api.media;
|
||||
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* This interface is used to provide {@link IMedia} implementations for {@link ItemStack}.
|
||||
*
|
||||
* @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(MediaProvider)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MediaProvider {
|
||||
/**
|
||||
* Produce an IMedia implementation from an ItemStack.
|
||||
*
|
||||
* @param stack The stack from which to extract the media information.
|
||||
* @return An {@link IMedia} implementation, or {@code null} if the item is not something you wish to handle
|
||||
* @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(MediaProvider)
|
||||
*/
|
||||
@Nullable
|
||||
IMedia getMedia(ItemStack stack);
|
||||
}
|
@@ -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}
|
||||
*
|
||||
|
@@ -4,17 +4,33 @@
|
||||
|
||||
package dan200.computercraft.api.turtle;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
|
||||
/**
|
||||
* An enum representing the two sides of the turtle that a turtle upgrade might reside.
|
||||
*/
|
||||
public enum TurtleSide {
|
||||
public enum TurtleSide implements StringRepresentable {
|
||||
/**
|
||||
* The turtle's left side (where the pickaxe usually is on a Wireless Mining Turtle).
|
||||
*/
|
||||
LEFT,
|
||||
LEFT("left"),
|
||||
|
||||
/**
|
||||
* The turtle's right side (where the modem usually is on a Wireless Mining Turtle).
|
||||
*/
|
||||
RIGHT,
|
||||
RIGHT("right");
|
||||
|
||||
public static final Codec<TurtleSide> CODEC = StringRepresentable.fromEnum(TurtleSide::values);
|
||||
|
||||
private final String name;
|
||||
|
||||
TurtleSide(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerializedName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ import java.util.function.Function;
|
||||
* @see ITurtleUpgrade
|
||||
* @see IPocketUpgrade
|
||||
*/
|
||||
public interface UpgradeType<T extends UpgradeBase> {
|
||||
public sealed interface UpgradeType<T extends UpgradeBase> permits UpgradeTypeImpl {
|
||||
/**
|
||||
* The codec to read and write this upgrade from a datapack.
|
||||
*
|
||||
|
@@ -12,7 +12,6 @@ import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.api.media.MediaProvider;
|
||||
import dan200.computercraft.api.media.PrintoutContents;
|
||||
import dan200.computercraft.api.network.PacketNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
@@ -60,8 +59,6 @@ public interface ComputerCraftAPIService {
|
||||
|
||||
int getBundledRedstoneOutput(Level world, BlockPos pos, Direction side);
|
||||
|
||||
void registerMediaProvider(MediaProvider provider);
|
||||
|
||||
PacketNetwork getWirelessNetwork(MinecraftServer server);
|
||||
|
||||
void registerAPIFactory(ILuaAPIFactory factory);
|
||||
|
@@ -11,6 +11,12 @@ plugins {
|
||||
id("cc-tweaked.publishing")
|
||||
}
|
||||
|
||||
sourceSets.client {
|
||||
java {
|
||||
exclude("dan200/computercraft/client/integration/emi")
|
||||
}
|
||||
}
|
||||
|
||||
minecraft {
|
||||
accessWideners(
|
||||
"src/main/resources/computercraft.accesswidener",
|
||||
|
@@ -6,12 +6,13 @@ package dan200.computercraft.client;
|
||||
|
||||
import com.mojang.blaze3d.audio.Channel;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.CableHighlightRenderer;
|
||||
import dan200.computercraft.client.render.ExtendedItemFrameRenderState;
|
||||
import dan200.computercraft.client.render.PocketItemRenderer;
|
||||
import dan200.computercraft.client.render.PrintoutItemRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorHighlightRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorRenderState;
|
||||
import dan200.computercraft.client.sound.SpeakerManager;
|
||||
@@ -29,16 +30,15 @@ import dan200.computercraft.shared.util.WorldUtil;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
|
||||
import net.minecraft.client.sounds.AudioStream;
|
||||
import net.minecraft.client.sounds.SoundEngine;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -91,9 +91,10 @@ public final class ClientHooks {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int light) {
|
||||
if (stack.getItem() instanceof PrintoutItem) {
|
||||
PrintoutItemRenderer.onRenderInFrame(transform, render, frame, stack, light);
|
||||
public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, ExtendedItemFrameRenderState state, int light) {
|
||||
if (state.printoutData != null) {
|
||||
transform.mulPose(Axis.ZP.rotationDegrees(frame.rotation * 360.0f / 8.0f));
|
||||
PrintoutItemRenderer.onRenderInFrame(transform, render, frame, state.printoutData, state.isBook, light);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -135,31 +136,20 @@ public final class ClientHooks {
|
||||
if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.holder().key().location()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional information about the game to the debug screen.
|
||||
*
|
||||
* @param addText A callback which adds a single line of text.
|
||||
*/
|
||||
public static void addGameDebugInfo(Consumer<String> addText) {
|
||||
if (MonitorBlockEntityRenderer.hasRenderedThisFrame() && Minecraft.getInstance().getDebugOverlay().showDebugScreen()) {
|
||||
addText.accept("[CC:T] Monitor renderer: " + MonitorBlockEntityRenderer.currentRenderer());
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
|
||||
public static BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
|
||||
// Only apply to cables which have both a cable and modem
|
||||
if (state.getBlock() != ModRegistry.Blocks.CABLE.get()
|
||||
|| !state.getValue(CableBlock.CABLE)
|
||||
|| state.getValue(CableBlock.MODEM) == CableModemVariant.None
|
||||
) {
|
||||
return null;
|
||||
return state;
|
||||
}
|
||||
|
||||
var hit = Minecraft.getInstance().hitResult;
|
||||
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return null;
|
||||
if (hit == null || hit.getType() != HitResult.Type.BLOCK) return state;
|
||||
var hitPos = ((BlockHitResult) hit).getBlockPos();
|
||||
|
||||
if (!hitPos.equals(pos)) return null;
|
||||
if (!hitPos.equals(pos)) return state;
|
||||
|
||||
return WorldUtil.isVecInside(CableShapes.getModemShape(state), hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()))
|
||||
? state.getBlock().defaultBlockState().setValue(CableBlock.MODEM, state.getValue(CableBlock.MODEM))
|
||||
|
@@ -4,61 +4,53 @@
|
||||
|
||||
package dan200.computercraft.client;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.client.StandaloneModel;
|
||||
import dan200.computercraft.api.client.turtle.*;
|
||||
import dan200.computercraft.client.gui.*;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.item.colour.PocketComputerLight;
|
||||
import dan200.computercraft.client.item.model.TurtleOverlayModel;
|
||||
import dan200.computercraft.client.item.properties.PocketComputerStateProperty;
|
||||
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.platform.ModelKey;
|
||||
import dan200.computercraft.client.render.CustomLecternRenderer;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
|
||||
import dan200.computercraft.client.turtle.TurtleModemModeller;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.CommandComputerCraft;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.media.items.DiskItem;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.color.item.ItemColor;
|
||||
import net.minecraft.client.color.item.ItemTintSource;
|
||||
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
|
||||
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
|
||||
import net.minecraft.client.gui.screens.MenuScreens;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
|
||||
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
|
||||
import net.minecraft.client.renderer.item.ItemProperties;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
|
||||
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
|
||||
import net.minecraft.client.resources.model.MissingBlockModel;
|
||||
import net.minecraft.client.resources.model.ModelBaker;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ResolvableModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
import net.minecraft.world.level.ItemLike;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
|
||||
@@ -72,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.
|
||||
*/
|
||||
@@ -83,25 +88,6 @@ public final class ClientRegistry {
|
||||
BlockEntityRenderers.register(ModRegistry.BlockEntities.LECTERN.get(), CustomLecternRenderer::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any client-side objects which must be done on the main thread.
|
||||
*
|
||||
* @param itemProperties Callback to register item properties.
|
||||
*/
|
||||
public static void registerMainThread(RegisterItemProperty itemProperties) {
|
||||
registerItemProperty(itemProperties, "state",
|
||||
new UnclampedPropertyFunction((stack, world, player, random) -> {
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
|
||||
}),
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
|
||||
);
|
||||
registerItemProperty(itemProperties, "coloured",
|
||||
(stack, world, player, random) -> DyedItemColor.getOrDefault(stack, -1) != -1 ? 1 : 0,
|
||||
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
|
||||
);
|
||||
}
|
||||
|
||||
public static void registerMenuScreens(RegisterMenuScreen register) {
|
||||
register.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
|
||||
register.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
|
||||
@@ -116,138 +102,102 @@ public final class ClientRegistry {
|
||||
<M extends AbstractContainerMenu, U extends Screen & MenuAccess<M>> void register(MenuType<? extends M> type, MenuScreens.ScreenConstructor<M, U> factory);
|
||||
}
|
||||
|
||||
public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.SPEAKER.get(), TurtleUpgradeModeller.sided(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
|
||||
));
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.WORKBENCH.get(), TurtleUpgradeModeller.sided(
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
|
||||
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
|
||||
));
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.WIRELESS_MODEM.get(), new TurtleModemModeller());
|
||||
register.register(ModRegistry.TurtleUpgradeTypes.TOOL.get(), TurtleUpgradeModeller.flatItem());
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
|
||||
var id = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
|
||||
for (var item : items) itemProperties.register(item.get(), id, getter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an item property via {@link ItemProperties#register}. Forge and Fabric expose different methods, so we
|
||||
* supply this via mod-loader-specific code.
|
||||
*/
|
||||
public interface RegisterItemProperty {
|
||||
void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
|
||||
}
|
||||
|
||||
public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
|
||||
register.accept(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 = {
|
||||
TurtleOverlay.ELF_MODEL,
|
||||
TurtleBlockEntityRenderer.NORMAL_TURTLE_MODEL,
|
||||
TurtleBlockEntityRenderer.ADVANCED_TURTLE_MODEL,
|
||||
TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
|
||||
MissingBlockModel.LOCATION,
|
||||
};
|
||||
|
||||
public static void registerExtraModels(Consumer<ResourceLocation> register, Collection<ResourceLocation> extraModels) {
|
||||
for (var model : EXTRA_MODELS) register.accept(model);
|
||||
extraModels.forEach(register);
|
||||
TurtleUpgradeModellers.getDependencies().forEach(register);
|
||||
}
|
||||
|
||||
public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
|
||||
register.accept(
|
||||
(stack, layer) -> layer == 1 ? DiskItem.getColour(stack) : -1,
|
||||
ModRegistry.Items.DISK.get()
|
||||
);
|
||||
|
||||
register.accept(
|
||||
(stack, layer) -> layer == 1 ? DyedItemColor.getOrDefault(stack, Colour.BLUE.getARGB()) : -1,
|
||||
ModRegistry.Items.TREASURE_DISK.get()
|
||||
);
|
||||
|
||||
register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get());
|
||||
register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
|
||||
|
||||
register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_NORMAL.get());
|
||||
register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_ADVANCED.get());
|
||||
}
|
||||
|
||||
private static int getPocketColour(ItemStack stack, int layer) {
|
||||
return switch (layer) {
|
||||
default -> -1;
|
||||
case 1 -> DyedItemColor.getOrDefault(stack, -1); // Frame colour
|
||||
case 2 -> { // Light colour
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getARGB() : FastColor.ARGB32.opaque(computer.getLightState());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static int getTurtleColour(ItemStack stack, int layer) {
|
||||
return layer == 0 ? DyedItemColor.getOrDefault(stack, -1) : -1;
|
||||
}
|
||||
|
||||
public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
|
||||
RenderTypes.registerShaders(resources, load);
|
||||
}
|
||||
|
||||
private record UnclampedPropertyFunction(
|
||||
ClampedItemPropertyFunction function
|
||||
) implements ClampedItemPropertyFunction {
|
||||
@Override
|
||||
public float unclampedCall(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
|
||||
return function.unclampedCall(stack, level, entity, layer);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public float call(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
|
||||
return function.unclampedCall(stack, level, entity, layer);
|
||||
}
|
||||
/**
|
||||
* Additional models to load.
|
||||
*
|
||||
* @param turtleOverlays The unbaked turtle models.
|
||||
* @param turtleUpgrades The unbaked turtle upgrades.
|
||||
* @see #gatherExtraModels(ResourceManager, Executor)
|
||||
* @see #registerExtraModels(RegisterExtraModels, ExtraModels)
|
||||
*/
|
||||
public record ExtraModels(
|
||||
Map<ResourceLocation, TurtleOverlay.Unbaked> turtleOverlays,
|
||||
Map<ResourceLocation, TurtleUpgradeModel.Unbaked> turtleUpgrades
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register client-side commands.
|
||||
* Gather the list of extra models to load.
|
||||
*
|
||||
* @param dispatcher The dispatcher to register the commands to.
|
||||
* @param sendError A function to send an error message.
|
||||
* @param <T> The type of the client-side command context.
|
||||
* @param resources The current resource manager.
|
||||
* @param executor The executor to schedule loading on.
|
||||
* @return A promise which contains our extra models.
|
||||
*/
|
||||
public static <T> void registerClientCommands(CommandDispatcher<T> dispatcher, BiConsumer<T, Component> sendError) {
|
||||
dispatcher.register(LiteralArgumentBuilder.<T>literal(CommandComputerCraft.CLIENT_OPEN_FOLDER)
|
||||
.requires(x -> Minecraft.getInstance().getSingleplayerServer() != null)
|
||||
.then(RequiredArgumentBuilder.<T, Integer>argument("computer_id", IntegerArgumentType.integer(0))
|
||||
.executes(c -> handleOpenComputerCommand(c.getSource(), sendError, c.getArgument("computer_id", Integer.class)))
|
||||
));
|
||||
public static CompletableFuture<ExtraModels> gatherExtraModels(ResourceManager resources, Executor executor) {
|
||||
var turtleOverlays = TurtleOverlayManager.loader().load(resources, executor);
|
||||
var turtleUpgrades = TurtleUpgradeModelManager.loader().load(resources, executor);
|
||||
return turtleOverlays.thenCombine(turtleUpgrades, ExtraModels::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the {@link CommandComputerCraft#CLIENT_OPEN_FOLDER} command.
|
||||
*
|
||||
* @param context The command context.
|
||||
* @param sendError A function to send an error message.
|
||||
* @param id The computer's id.
|
||||
* @param <T> The type of the client-side command context.
|
||||
* @return {@code 1} if a folder was opened, {@code 0} otherwise.
|
||||
* A callback used to register a model for a {@link ModelKey}.
|
||||
*/
|
||||
private static <T> int handleOpenComputerCommand(T context, BiConsumer<T, Component> sendError, int id) {
|
||||
var server = Minecraft.getInstance().getSingleplayerServer();
|
||||
if (server == null) {
|
||||
sendError.accept(context, Component.literal("Not on a single-player server"));
|
||||
return 0;
|
||||
public interface RegisterExtraModels {
|
||||
default <U extends ResolvableModel, T> void register(ModelKey<T> key, U unbaked, BiFunction<U, ModelBaker, T> bake) {
|
||||
register(key, unbaked, ResolvableModel::resolveDependencies, bake);
|
||||
}
|
||||
|
||||
var file = new File(ServerContext.get(server).storageDir().toFile(), "computer/" + id);
|
||||
if (!file.isDirectory()) {
|
||||
sendError.accept(context, Component.literal("Computer's folder does not exist"));
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* Register an extra model.
|
||||
* <p>
|
||||
* This accepts functions to resolve dependencies and bake the model. While this would be conceptually nicer as
|
||||
* an interface, it would require multiple adaptors to convert between "upgrade model", "a"bstract model" and
|
||||
* "platform-specific model", so working with functions is cleaner.
|
||||
*
|
||||
* @param key The model key for this model.
|
||||
* @param unbaked The unbaked model.
|
||||
* @param resolve The function to resolve dependencies for this model.
|
||||
* @param bake The function to bake this model.
|
||||
* @param <U> The type of unbaked model.
|
||||
* @param <T> The type of baked model.
|
||||
*/
|
||||
<U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake);
|
||||
}
|
||||
|
||||
Util.getPlatform().openFile(file);
|
||||
return 1;
|
||||
public static void registerExtraModels(RegisterExtraModels register, ExtraModels models) {
|
||||
for (var model : EXTRA_MODELS) {
|
||||
register.register(getModel(model), model, (id, r) -> r.markDependency(id), StandaloneModel::of);
|
||||
}
|
||||
TurtleOverlayManager.loader().register(register, models.turtleOverlays());
|
||||
TurtleUpgradeModelManager.loader().register(register, models.turtleUpgrades());
|
||||
}
|
||||
|
||||
public static void registerItemModels(BiConsumer<ResourceLocation, MapCodec<? extends ItemModel.Unbaked>> register) {
|
||||
register.accept(TurtleOverlayModel.ID, TurtleOverlayModel.CODEC);
|
||||
register.accept(dan200.computercraft.client.item.model.TurtleUpgradeModel.ID, dan200.computercraft.client.item.model.TurtleUpgradeModel.CODEC);
|
||||
}
|
||||
|
||||
public static void registerItemColours(BiConsumer<ResourceLocation, MapCodec<? extends ItemTintSource>> register) {
|
||||
register.accept(PocketComputerLight.ID, PocketComputerLight.CODEC);
|
||||
}
|
||||
|
||||
public static void registerSelectItemProperties(BiConsumer<ResourceLocation, SelectItemModelProperty.Type<?, ?>> register) {
|
||||
register.accept(PocketComputerStateProperty.ID, PocketComputerStateProperty.TYPE);
|
||||
}
|
||||
|
||||
public static void registerConditionalItemProperties(BiConsumer<ResourceLocation, MapCodec<? extends ConditionalItemModelProperty>> register) {
|
||||
register.accept(TurtleShowElfOverlay.ID, TurtleShowElfOverlay.CODEC);
|
||||
}
|
||||
|
||||
public interface RegisterPictureInPictureRenderer {
|
||||
<T extends PictureInPictureRenderState> void register(Class<T> state, Function<MultiBufferSource.BufferSource, PictureInPictureRenderer<T>> factory);
|
||||
}
|
||||
|
||||
public static void registerPictureInPictureRenderers(RegisterPictureInPictureRenderer register) {
|
||||
register.register(PrintoutScreen.PrintoutRenderState.class, PrintoutScreen.PrintoutPictureRenderer::new);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -99,7 +99,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|
||||
|
||||
if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
|
||||
new ItemToast(minecraft(), displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
|
||||
.showOrReplace(minecraft().getToasts());
|
||||
.showOrReplace(minecraft().getToastManager());
|
||||
uploadNagDeadline = Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,15 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|
||||
return super.mouseReleased(x, y, button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
// Reimplement ContainerEventHandler.mouseScrolled, as AbstractContainerScreen overrides it.
|
||||
var child = getChildAt(mouseX, mouseY);
|
||||
if (child.isPresent() && child.get().mouseScrolled(mouseX, mouseY, scrollX, scrollY)) return true;
|
||||
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
|
||||
super.render(graphics, mouseX, mouseY, partialTicks);
|
||||
|
@@ -6,11 +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.RenderTypes;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
@@ -40,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 spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES);
|
||||
var computerTextures = GuiSprites.getComputerTextures(family);
|
||||
|
||||
ComputerBorderRenderer.render(
|
||||
spriteRenderer, computerTextures,
|
||||
terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
|
||||
graphics.blitSprite(
|
||||
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
|
||||
);
|
||||
ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
|
||||
graphics.flush(); // Flush to ensure background textures are drawn before foreground.
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ package dan200.computercraft.client.gui;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -23,7 +24,7 @@ public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
|
||||
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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.
|
||||
*
|
||||
|
@@ -5,9 +5,11 @@
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.toasts.Toast;
|
||||
import net.minecraft.client.gui.components.toasts.ToastComponent;
|
||||
import net.minecraft.client.gui.components.toasts.ToastManager;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
@@ -35,8 +37,9 @@ public class ItemToast implements Toast {
|
||||
private final Object token;
|
||||
private final int width;
|
||||
|
||||
private boolean isNew = true;
|
||||
private long firstDisplay;
|
||||
private boolean changed = true;
|
||||
private long lastChanged;
|
||||
private Visibility visibility = Visibility.HIDE;
|
||||
|
||||
public ItemToast(Minecraft minecraft, ItemStack stack, Component title, Component message, Object token) {
|
||||
this.stack = stack;
|
||||
@@ -48,10 +51,10 @@ public class ItemToast implements Toast {
|
||||
width = Math.max(MAX_LINE_SIZE, this.message.stream().mapToInt(font::width).max().orElse(MAX_LINE_SIZE)) + MARGIN * 3 + IMAGE_SIZE;
|
||||
}
|
||||
|
||||
public void showOrReplace(ToastComponent toasts) {
|
||||
public void showOrReplace(ToastManager toasts) {
|
||||
var existing = toasts.getToast(ItemToast.class, getToken());
|
||||
if (existing != null) {
|
||||
existing.isNew = true;
|
||||
existing.changed = true;
|
||||
} else {
|
||||
toasts.addToast(this);
|
||||
}
|
||||
@@ -73,28 +76,22 @@ public class ItemToast implements Toast {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Visibility render(GuiGraphics graphics, ToastComponent component, long time) {
|
||||
if (isNew) {
|
||||
public Visibility getWantedVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
firstDisplay = time;
|
||||
isNew = false;
|
||||
@Override
|
||||
public void update(ToastManager toastManager, long time) {
|
||||
if (changed) {
|
||||
lastChanged = time;
|
||||
changed = false;
|
||||
}
|
||||
visibility = time - lastChanged < DISPLAY_TIME * toastManager.getNotificationDisplayTimeMultiplier() ? Visibility.SHOW : Visibility.HIDE;
|
||||
}
|
||||
|
||||
if (width == 160 && message.size() <= 1) {
|
||||
graphics.blitSprite(TEXTURE, 0, 0, width, height());
|
||||
} else {
|
||||
|
||||
var height = height();
|
||||
|
||||
var bottom = Math.min(4, height - 28);
|
||||
renderBackgroundRow(graphics, width, 0, 0, 28);
|
||||
|
||||
for (var i = 28; i < height - bottom; i += 10) {
|
||||
renderBackgroundRow(graphics, width, 16, i, Math.min(16, height - i - bottom));
|
||||
}
|
||||
|
||||
renderBackgroundRow(graphics, width, 32 - bottom, height - bottom, bottom);
|
||||
}
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, Font font, long time) {
|
||||
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, 0, width(), height());
|
||||
|
||||
var textX = MARGIN;
|
||||
if (!stack.isEmpty()) {
|
||||
@@ -102,23 +99,9 @@ public class ItemToast implements Toast {
|
||||
graphics.renderFakeItem(stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE);
|
||||
}
|
||||
|
||||
graphics.drawString(component.getMinecraft().font, title, textX, MARGIN, 0xff500050, false);
|
||||
graphics.drawString(font, title, textX, MARGIN, 0xff500050, false);
|
||||
for (var i = 0; i < message.size(); ++i) {
|
||||
graphics.drawString(component.getMinecraft().font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
|
||||
graphics.drawString(font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
|
||||
}
|
||||
|
||||
return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
|
||||
}
|
||||
|
||||
private static void renderBackgroundRow(GuiGraphics graphics, int x, int u, int y, int height) {
|
||||
var leftOffset = u == 0 ? 20 : 5;
|
||||
var rightOffset = Math.min(60, x - leftOffset);
|
||||
|
||||
graphics.blitSprite(TEXTURE, 160, 32, 0, u, 0, y, leftOffset, height);
|
||||
for (var k = leftOffset; k < x - rightOffset; k += 64) {
|
||||
graphics.blitSprite(TEXTURE, 160, 32, 32, u, k, y, Math.min(64, x - k - rightOffset), height);
|
||||
}
|
||||
|
||||
graphics.blitSprite(TEXTURE, 160, 32, 160 - rightOffset, u, x - rightOffset, y, rightOffset, height);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.ScrollWheelHandler;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
|
||||
@@ -32,6 +33,8 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
|
||||
private final Terminal terminalData;
|
||||
private @Nullable TerminalWidget terminal;
|
||||
|
||||
private final ScrollWheelHandler scrollHandler = new ScrollWheelHandler();
|
||||
|
||||
public NoTermComputerScreen(T menu, Inventory player, Component title) {
|
||||
super(title);
|
||||
this.menu = menu;
|
||||
@@ -67,7 +70,12 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
Objects.requireNonNull(minecraft().player).getInventory().swapPaint(scrollY);
|
||||
var direction = scrollHandler.onMouseScroll(scrollX, scrollY);
|
||||
var inventory = Objects.requireNonNull(minecraft().player).getInventory();
|
||||
inventory.setSelectedSlot(ScrollWheelHandler.getNextScrollWheelSelection(
|
||||
direction.y == 0 ? -direction.x : direction.y, inventory.getSelectedSlot(), Inventory.getSelectionSize()
|
||||
));
|
||||
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@@ -100,7 +108,7 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
|
||||
var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8));
|
||||
var y = 10;
|
||||
for (var line : lines) {
|
||||
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFF, true);
|
||||
graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFFFF, true);
|
||||
y += 9;
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.MultiLineLabel;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -87,12 +88,13 @@ public final class OptionScreen extends Screen {
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
|
||||
// Render the actual texture.
|
||||
graphics.blit(BACKGROUND, x, y, 0, 0, innerWidth, PADDING);
|
||||
graphics.blit(BACKGROUND,
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND,
|
||||
x, y + PADDING, 0, PADDING, innerWidth, innerHeight - PADDING * 2,
|
||||
innerWidth, PADDING
|
||||
innerWidth, PADDING,
|
||||
256, 256
|
||||
);
|
||||
graphics.blit(BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
|
||||
|
||||
assertNonNull(messageRenderer).renderLeftAlignedNoShadow(graphics, x + PADDING, y + PADDING, FONT_HEIGHT, 0x404040);
|
||||
super.render(graphics, mouseX, mouseY, partialTicks);
|
||||
|
@@ -7,6 +7,7 @@ package dan200.computercraft.client.gui;
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -23,9 +24,11 @@ public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
|
||||
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||
|
||||
if (getMenu().isPrinting()) graphics.blit(BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45);
|
||||
if (getMenu().isPrinting()) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -4,28 +4,37 @@
|
||||
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.client.render.PrintoutRenderer;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
|
||||
import net.minecraft.client.gui.render.state.GuiElementRenderState;
|
||||
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerListener;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Matrix3x2f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
/**
|
||||
* The GUI for printed pages and books.
|
||||
*
|
||||
* @see dan200.computercraft.client.render.PrintoutRenderer
|
||||
* @see PrintoutRenderer
|
||||
*/
|
||||
public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu> implements ContainerListener {
|
||||
private PrintoutInfo printout = PrintoutInfo.DEFAULT;
|
||||
@@ -113,13 +122,11 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
// Push the printout slightly forward, to avoid clipping into the background.
|
||||
graphics.pose().pushPose();
|
||||
graphics.pose().translate(0, 0, 1);
|
||||
|
||||
drawBorder(graphics.pose(), graphics.bufferSource(), leftPos, topPos, 0, page, printout.pages(), printout.book(), FULL_BRIGHT_LIGHTMAP);
|
||||
drawText(graphics.pose(), graphics.bufferSource(), leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, printout.text(), printout.colour());
|
||||
|
||||
graphics.pose().popPose();
|
||||
graphics.guiRenderState.submitPicturesInPictureState(new PrintoutRenderState(
|
||||
leftPos - COVER_SIZE - 32, leftPos + X_SIZE + COVER_SIZE + 32,
|
||||
topPos - COVER_SIZE, topPos + Y_SIZE + COVER_SIZE,
|
||||
printout, page, new Matrix3x2f(graphics.pose()), graphics.scissorStack.peek()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,4 +151,56 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
|
||||
return new PrintoutInfo(pages, book, text, colours);
|
||||
}
|
||||
}
|
||||
|
||||
public record PrintoutRenderState(
|
||||
int x0, int x1, int y0, int y1, PrintoutInfo printout, int page, Matrix3x2f pose,
|
||||
@Nullable ScreenRectangle scissorArea, @Nullable ScreenRectangle bounds
|
||||
) implements PictureInPictureRenderState {
|
||||
private PrintoutRenderState(
|
||||
int x0, int x1, int y0, int y1, PrintoutInfo printout, int page, Matrix3x2f pose, @Nullable ScreenRectangle scissorArea
|
||||
) {
|
||||
this(x0, x1, y0, y1, printout, page, pose, scissorArea, PictureInPictureRenderState.getBounds(x0, x1, y0, y1, scissorArea));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float scale() {
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PIP renderer for printouts.
|
||||
* <p>
|
||||
* We prefer using a PIP (rather than a {@link GuiElementRenderState}), as {@link PrintoutRenderer} renders with
|
||||
* multiple z-levels.
|
||||
*/
|
||||
public static final class PrintoutPictureRenderer extends PictureInPictureRenderer<PrintoutRenderState> {
|
||||
public PrintoutPictureRenderer(MultiBufferSource.BufferSource bufferSource) {
|
||||
super(bufferSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderToTexture(PrintoutRenderState state, PoseStack pose) {
|
||||
pose.pushPose();
|
||||
pose.translate(-0.5f * X_SIZE, -(Y_SIZE + COVER_SIZE), 0);
|
||||
pose.scale(1.0f, 1.0f, -1.0f);
|
||||
|
||||
drawBorder(pose, bufferSource, 0, 0, 0, state.page(), state.printout().pages(), state.printout().book(), LightTexture.FULL_BRIGHT);
|
||||
drawText(
|
||||
pose, bufferSource, X_TEXT_MARGIN, Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * state.page(), LightTexture.FULL_BRIGHT,
|
||||
state.printout().text(), state.printout().colour()
|
||||
);
|
||||
pose.popPose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<PrintoutRenderState> getRenderStateClass() {
|
||||
return PrintoutRenderState.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTextureLabel() {
|
||||
return "Printout";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.RenderTypes;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -49,8 +49,11 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
@Override
|
||||
protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
|
||||
var advanced = family == ComputerFamily.ADVANCED;
|
||||
var texture = advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL;
|
||||
graphics.blit(texture, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0, 0, TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE);
|
||||
graphics.blit(
|
||||
RenderPipelines.GUI_TEXTURED, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL,
|
||||
leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0,
|
||||
TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE
|
||||
);
|
||||
|
||||
// Render selected slot
|
||||
var slot = getMenu().getSelectedSlot();
|
||||
@@ -58,14 +61,15 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
var slotX = slot % 4;
|
||||
var slotY = slot / 4;
|
||||
graphics.blitSprite(
|
||||
advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
|
||||
RenderPipelines.GUI_TEXTURED, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 22, 22
|
||||
);
|
||||
}
|
||||
|
||||
// Render sidebar
|
||||
var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES);
|
||||
ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset);
|
||||
graphics.flush(); // Flush to ensure background textures are drawn before foreground.
|
||||
graphics.blitSprite(
|
||||
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();
|
||||
|
@@ -4,12 +4,12 @@
|
||||
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -48,10 +48,7 @@ public class DynamicImageButton extends Button {
|
||||
setTooltip(message.tooltip());
|
||||
|
||||
var texture = this.texture.get(isHoveredOrFocused());
|
||||
|
||||
RenderSystem.disableDepthTest();
|
||||
graphics.blitSprite(texture, getX(), getY(), 0, width, height);
|
||||
RenderSystem.enableDepthTest();
|
||||
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, texture, getX(), getY(), width, height);
|
||||
}
|
||||
|
||||
public record HintedMessage(Component message, Tooltip tooltip) {
|
||||
|
@@ -4,7 +4,9 @@
|
||||
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
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;
|
||||
@@ -14,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;
|
||||
@@ -83,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;
|
||||
}
|
||||
@@ -119,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;
|
||||
@@ -254,12 +264,23 @@ public class TerminalWidget extends AbstractWidget {
|
||||
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
|
||||
if (!visible) return;
|
||||
|
||||
var emitter = FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), graphics.bufferSource().getBuffer(RenderTypes.TERMINAL));
|
||||
var scissor = graphics.scissorStack.peek();
|
||||
var terminalPose = new Matrix3x2f(graphics.pose());
|
||||
var terminalTextures = TextureSetup.singleTextureWithLightmap(graphics.minecraft.getTextureManager().getTexture(FixedWidthFontRenderer.FONT).getTextureView());
|
||||
|
||||
FixedWidthFontRenderer.drawTerminal(
|
||||
emitter,
|
||||
(float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
|
||||
);
|
||||
graphics.guiRenderState.submitGuiElement(new TerminalBackgroundRenderState(
|
||||
innerX, innerY, terminal, terminalPose, terminalTextures,
|
||||
maybeIntersect(scissor, new ScreenRectangle(
|
||||
innerX, innerY, terminal.getWidth() * FONT_WIDTH, terminal.getHeight() * FONT_HEIGHT).transformMaxBounds(graphics.pose())
|
||||
),
|
||||
scissor
|
||||
));
|
||||
|
||||
graphics.guiRenderState.submitGuiElement(new TerminalTextRenderState(
|
||||
innerX, innerY, terminal, terminalPose, terminalTextures,
|
||||
maybeIntersect(scissor, new ScreenRectangle(getX(), getY(), getWidth(), getHeight()).transformMaxBounds(graphics.pose())),
|
||||
scissor
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -274,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,12 +6,11 @@ package dan200.computercraft.client.integration;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.irisshaders.iris.api.v0.IrisApi;
|
||||
import net.irisshaders.iris.api.v0.IrisTextVertexSink;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
@@ -31,33 +30,31 @@ public class IrisShaderMod implements ShaderMod.Provider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return IrisApi.getInstance().getMinorApiRevision() >= 1
|
||||
? new IrisQuadEmitter(vertexCount, makeBuffer)
|
||||
: super.getQuadEmitter(vertexCount, makeBuffer);
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return new IrisQuadEmitter(quadCount, makeBuffer);
|
||||
}
|
||||
|
||||
private static final class IrisQuadEmitter implements DirectFixedWidthFontRenderer.QuadEmitter {
|
||||
private static final class IrisQuadEmitter extends DirectFixedWidthFontRenderer.QuadEmitter {
|
||||
private final IrisTextVertexSink sink;
|
||||
|
||||
private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, makeBuffer);
|
||||
private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> builder) {
|
||||
sink = IrisApi.getInstance().createTextVertexSink(vertexCount, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer byteBuffer() {
|
||||
return sink.getUnderlyingByteBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
sink.quad(x1, y1, x2, y2, z, colour, u1, v1, u2, v2, LightTexture.FULL_BRIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexFormat format() {
|
||||
return sink.getUnderlyingVertexFormat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer buffer() {
|
||||
return sink.getUnderlyingByteBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
sink.quad(x1, y1, x2, y2, z, FastColor.ABGR32.fromArgb32(colour), u1, v1, u2, v2, RenderTypes.FULL_BRIGHT_LIGHTMAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,15 +4,15 @@
|
||||
|
||||
package dan200.computercraft.client.integration;
|
||||
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.TERMINAL_TEXT;
|
||||
|
||||
/**
|
||||
* Find the currently loaded shader mod (if present) and provides utilities for interacting with it.
|
||||
*/
|
||||
@@ -31,16 +31,14 @@ public class ShaderMod {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an appropriate quad emitter for use with {@link DirectVertexBuffer} and {@link DirectFixedWidthFontRenderer} .
|
||||
* Get an appropriate quad emitter for use with a vertex buffer and {@link DirectFixedWidthFontRenderer} .
|
||||
*
|
||||
* @param vertexCount The number of vertices.
|
||||
* @param makeBuffer A function to allocate a temporary buffer.
|
||||
* @param quadCount The number of quads.
|
||||
* @param makeBuffer A function to allocate a temporary buffer.
|
||||
* @return The quad emitter.
|
||||
*/
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(
|
||||
makeBuffer.apply(RenderTypes.TERMINAL.format().getVertexSize() * vertexCount * 4)
|
||||
);
|
||||
public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int quadCount, IntFunction<ByteBuffer> makeBuffer) {
|
||||
return new DirectFixedWidthFontRenderer.ByteBufferEmitter(makeBuffer.apply(TERMINAL_TEXT.format().getVertexSize() * quadCount * 4));
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
|
@@ -8,14 +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.media.items.DiskItem;
|
||||
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;
|
||||
@@ -23,6 +23,7 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -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(DiskItem.getColour(stack));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.item.colour;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.pocket.PocketComputerData;
|
||||
import net.minecraft.client.color.item.ItemTintSource;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.ExtraCodecs;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An {@link ItemTintSource} that returns the pocket computer's {@linkplain PocketComputerData#getLightState() light
|
||||
* colour}.
|
||||
*
|
||||
* @param defaultColour The default colour, if the light is not currently on.
|
||||
*/
|
||||
public record PocketComputerLight(int defaultColour) implements ItemTintSource {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_light");
|
||||
public static final MapCodec<PocketComputerLight> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
ExtraCodecs.RGB_COLOR_CODEC.fieldOf("default").forGetter(PocketComputerLight::defaultColour)
|
||||
).apply(instance, PocketComputerLight::new));
|
||||
|
||||
@Override
|
||||
public int calculate(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder) {
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
return computer == null || computer.getLightState() == -1 ? defaultColour : ARGB.opaque(computer.getLightState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<? extends ItemTintSource> type() {
|
||||
return CODEC;
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.item.model;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An {@link ItemModel} that renders the {@linkplain TurtleOverlay turtle overlay}.
|
||||
*
|
||||
* @param transforms The item transformations from the base model.
|
||||
* @see TurtleOverlay#model()
|
||||
*/
|
||||
public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/overlay");
|
||||
public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
ResourceLocation.CODEC.fieldOf("transforms").forGetter(Unbaked::base)
|
||||
).apply(instance, Unbaked::new));
|
||||
|
||||
@Override
|
||||
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int light) {
|
||||
var overlay = TurtleItem.getOverlay(stack);
|
||||
if (overlay == null) return;
|
||||
|
||||
state.appendModelIdentityElement(this);
|
||||
state.appendModelIdentityElement(overlay);
|
||||
|
||||
var layer = state.newLayer();
|
||||
TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), overlay).model().setupItemLayer(layer);
|
||||
layer.setTransform(transforms().getTransform(context));
|
||||
}
|
||||
|
||||
public record Unbaked(ResourceLocation base) implements ItemModel.Unbaked {
|
||||
@Override
|
||||
public MapCodec<Unbaked> type() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemModel bake(BakingContext bakingContext) {
|
||||
return new TurtleOverlayModel(bakingContext.blockModelBaker().getModel(base).getTopTransforms());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.item.model;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
|
||||
import net.minecraft.client.renderer.item.ItemModel;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An {@link ItemModel} that renders a turtle upgrade, using its {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModel}.
|
||||
*
|
||||
* @param side The side the upgrade resides on.
|
||||
* @param base The base model. Only used to provide item transforms.
|
||||
*/
|
||||
public record TurtleUpgradeModel(TurtleSide side, ItemTransforms base) implements ItemModel {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/upgrade");
|
||||
public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
TurtleSide.CODEC.fieldOf("side").forGetter(Unbaked::side),
|
||||
ResourceLocation.CODEC.fieldOf("transforms").forGetter(Unbaked::base)
|
||||
).apply(instance, Unbaked::new));
|
||||
|
||||
@Override
|
||||
public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int seed) {
|
||||
var upgrade = TurtleItem.getUpgradeWithData(stack, side);
|
||||
if (upgrade == null) return;
|
||||
|
||||
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
|
||||
.renderForItem(upgrade, side, state, resolver, base.getTransform(context), seed);
|
||||
}
|
||||
|
||||
public record Unbaked(TurtleSide side, ResourceLocation base) implements ItemModel.Unbaked {
|
||||
@Override
|
||||
public MapCodec<Unbaked> type() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemModel bake(BakingContext bakingContext) {
|
||||
return new TurtleUpgradeModel(side, bakingContext.blockModelBaker().getModel(base).getTopTransforms());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolveDependencies(Resolver resolver) {
|
||||
resolver.markDependency(base);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.item.properties;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link SelectItemModelProperty} that returns the pocket computer's current state.
|
||||
*/
|
||||
public final class PocketComputerStateProperty implements SelectItemModelProperty<ComputerState> {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_state");
|
||||
private static final PocketComputerStateProperty INSTANCE = new PocketComputerStateProperty();
|
||||
public static final MapCodec<PocketComputerStateProperty> CODEC = MapCodec.unit(INSTANCE);
|
||||
public static final Type<PocketComputerStateProperty, ComputerState> TYPE = Type.create(CODEC, ComputerState.CODEC);
|
||||
|
||||
private PocketComputerStateProperty() {
|
||||
}
|
||||
|
||||
public static PocketComputerStateProperty create() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComputerState get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
|
||||
var computer = ClientPocketComputers.get(stack);
|
||||
return computer == null ? ComputerState.OFF : computer.getState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Codec<ComputerState> valueCodec() {
|
||||
return ComputerState.CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends SelectItemModelProperty<ComputerState>, ComputerState> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.item.properties;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* An item property that determines whether the turtle's current {@linkplain TurtleOverlay overlay} is compatible
|
||||
* with the Christmas overlay.
|
||||
*
|
||||
* @see TurtleOverlay#showElfOverlay()
|
||||
*/
|
||||
public class TurtleShowElfOverlay implements ConditionalItemModelProperty {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/show_elf_overlay");
|
||||
private static final TurtleShowElfOverlay INSTANCE = new TurtleShowElfOverlay();
|
||||
public static final MapCodec<TurtleShowElfOverlay> CODEC = MapCodec.unit(INSTANCE);
|
||||
|
||||
@Override
|
||||
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
|
||||
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
|
||||
return overlay == null || overlay.showElfOverlay();
|
||||
}
|
||||
|
||||
public static TurtleShowElfOverlay create() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<? extends ConditionalItemModelProperty> type() {
|
||||
return CODEC;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -17,9 +17,9 @@ import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.client.resources.model.Material;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.InventoryMenu;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
|
||||
/**
|
||||
@@ -34,11 +34,11 @@ public class LecternPocketModel {
|
||||
public static final ResourceLocation TEXTURE_FRAME = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
|
||||
public static final ResourceLocation TEXTURE_LIGHT = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_light");
|
||||
|
||||
private static final Material MATERIAL_NORMAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_NORMAL);
|
||||
private static final Material MATERIAL_ADVANCED = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_ADVANCED);
|
||||
private static final Material MATERIAL_COLOUR = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_COLOUR);
|
||||
private static final Material MATERIAL_FRAME = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_FRAME);
|
||||
private static final Material MATERIAL_LIGHT = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_LIGHT);
|
||||
private static final Material MATERIAL_NORMAL = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_NORMAL);
|
||||
private static final Material MATERIAL_ADVANCED = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_ADVANCED);
|
||||
private static final Material MATERIAL_COLOUR = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_COLOUR);
|
||||
private static final Material MATERIAL_FRAME = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_FRAME);
|
||||
private static final Material MATERIAL_LIGHT = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_LIGHT);
|
||||
|
||||
// The size of the terminal within the model.
|
||||
public static final float TERM_WIDTH = 12.0f / 32.0f;
|
||||
|
@@ -13,9 +13,9 @@ import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.model.geom.PartPose;
|
||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
|
||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.client.resources.model.Material;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.InventoryMenu;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.util.List;
|
||||
*/
|
||||
public class LecternPrintoutModel {
|
||||
public static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/printout");
|
||||
public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE);
|
||||
public static final Material MATERIAL = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE);
|
||||
|
||||
private static final int TEXTURE_WIDTH = 32;
|
||||
private static final int TEXTURE_HEIGHT = 32;
|
||||
|
@@ -1,92 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.model.turtle;
|
||||
|
||||
import com.mojang.math.Transformation;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.block.model.FaceBakery;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.core.Direction;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Applies a {@link Transformation} (or rather a {@link Matrix4f}) to a list of {@link BakedQuad}s.
|
||||
* <p>
|
||||
* This does a little bit of magic compared with other system (i.e. Forge's {@code QuadTransformers}), as it needs to
|
||||
* handle flipping models upside down.
|
||||
* <p>
|
||||
* This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
|
||||
*/
|
||||
public class ModelTransformer {
|
||||
private static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
|
||||
|
||||
private static final int STRIDE = FaceBakery.VERTEX_INT_SIZE;
|
||||
private static final int POS_OFFSET = 0;
|
||||
|
||||
protected final Matrix4f transformation;
|
||||
protected final boolean invert;
|
||||
private @Nullable TransformedQuads cache;
|
||||
|
||||
public ModelTransformer(Transformation transformation) {
|
||||
this.transformation = transformation.getMatrix();
|
||||
invert = transformation.getMatrix().determinant() < 0;
|
||||
}
|
||||
|
||||
public List<BakedQuad> transform(List<BakedQuad> quads) {
|
||||
if (quads.isEmpty()) return List.of();
|
||||
|
||||
// We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
|
||||
// so it's not worth being smarter here.
|
||||
var cache = this.cache;
|
||||
if (cache != null && quads.equals(cache.original())) return cache.transformed();
|
||||
|
||||
List<BakedQuad> transformed = new ArrayList<>(quads.size());
|
||||
for (var quad : quads) transformed.add(transformQuad(quad));
|
||||
this.cache = new TransformedQuads(quads, transformed);
|
||||
return transformed;
|
||||
}
|
||||
|
||||
private BakedQuad transformQuad(BakedQuad quad) {
|
||||
var inputData = quad.getVertices();
|
||||
var outputData = new int[inputData.length];
|
||||
for (var i = 0; i < 4; i++) {
|
||||
var inStart = STRIDE * i;
|
||||
// Reverse the order of the quads if we're inverting
|
||||
var outStart = getVertexOffset(i, invert);
|
||||
System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
|
||||
|
||||
// Apply the matrix to our position
|
||||
var inPosStart = inStart + POS_OFFSET;
|
||||
var outPosStart = outStart + POS_OFFSET;
|
||||
|
||||
var x = Float.intBitsToFloat(inputData[inPosStart]);
|
||||
var y = Float.intBitsToFloat(inputData[inPosStart + 1]);
|
||||
var z = Float.intBitsToFloat(inputData[inPosStart + 2]);
|
||||
|
||||
// Transform the position
|
||||
var pos = new Vector4f(x, y, z, 1);
|
||||
transformation.transformProject(pos);
|
||||
|
||||
outputData[outPosStart] = Float.floatToRawIntBits(pos.x());
|
||||
outputData[outPosStart + 1] = Float.floatToRawIntBits(pos.y());
|
||||
outputData[outPosStart + 2] = Float.floatToRawIntBits(pos.z());
|
||||
}
|
||||
|
||||
var direction = Direction.rotate(transformation, quad.getDirection());
|
||||
return new BakedQuad(outputData, quad.getTintIndex(), direction, quad.getSprite(), quad.isShade());
|
||||
}
|
||||
|
||||
public static int getVertexOffset(int vertex, boolean invert) {
|
||||
return (invert ? ModelTransformer.INVERSE_ORDER[vertex] : vertex) * ModelTransformer.STRIDE;
|
||||
}
|
||||
|
||||
private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
|
||||
}
|
||||
}
|
@@ -1,146 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.client.model.turtle;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import dan200.computercraft.shared.util.DataComponentUtil;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Combines several individual models together to form a turtle.
|
||||
*
|
||||
* @param <T> The type of the resulting "baked model".
|
||||
*/
|
||||
public final class TurtleModelParts<T> {
|
||||
private static final Transformation identity, flip;
|
||||
|
||||
static {
|
||||
var stack = new PoseStack();
|
||||
stack.translate(0.5f, 0.5f, 0.5f);
|
||||
stack.scale(1, -1, 1);
|
||||
stack.translate(-0.5f, -0.5f, -0.5f);
|
||||
|
||||
identity = Transformation.identity();
|
||||
flip = new Transformation(stack.last().pose());
|
||||
}
|
||||
|
||||
private record Combination(
|
||||
boolean colour,
|
||||
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade,
|
||||
@Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
|
||||
@Nullable TurtleOverlay overlay,
|
||||
boolean christmas,
|
||||
boolean flip
|
||||
) {
|
||||
}
|
||||
|
||||
private final BakedModel familyModel;
|
||||
private final BakedModel colourModel;
|
||||
private final Function<TransformedModel, BakedModel> transformer;
|
||||
private final Function<Combination, T> buildModel;
|
||||
|
||||
/**
|
||||
* A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed
|
||||
* instances, reducing memory usage and hopefully ensuring their caches are hit more often!
|
||||
*/
|
||||
private final Map<TransformedModel, BakedModel> transformCache = CacheBuilder.newBuilder()
|
||||
.concurrencyLevel(1)
|
||||
.expireAfterAccess(30, TimeUnit.SECONDS)
|
||||
.<TransformedModel, BakedModel>build()
|
||||
.asMap();
|
||||
|
||||
/**
|
||||
* A cache of {@link Combination}s to the combined model.
|
||||
*/
|
||||
private final Map<Combination, T> modelCache = CacheBuilder.newBuilder()
|
||||
.concurrencyLevel(1)
|
||||
.expireAfterAccess(30, TimeUnit.SECONDS)
|
||||
.<Combination, T>build()
|
||||
.asMap();
|
||||
|
||||
public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function<List<BakedModel>, T> combineModel) {
|
||||
this.familyModel = familyModel;
|
||||
this.colourModel = colourModel;
|
||||
this.transformer = x -> transformer.transform(x.model(), x.matrix());
|
||||
buildModel = x -> combineModel.apply(buildModel(x));
|
||||
}
|
||||
|
||||
public T getModel(ItemStack stack) {
|
||||
var combination = getCombination(stack);
|
||||
return modelCache.computeIfAbsent(combination, buildModel);
|
||||
}
|
||||
|
||||
private Combination getCombination(ItemStack stack) {
|
||||
var christmas = Holiday.getCurrent() == Holiday.CHRISTMAS;
|
||||
var leftUpgrade = TurtleItem.getUpgradeWithData(stack, TurtleSide.LEFT);
|
||||
var rightUpgrade = TurtleItem.getUpgradeWithData(stack, TurtleSide.RIGHT);
|
||||
var overlay = TurtleItem.getOverlay(stack);
|
||||
var label = DataComponentUtil.getCustomName(stack);
|
||||
var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm"));
|
||||
|
||||
return new Combination(stack.has(DataComponents.DYED_COLOR), leftUpgrade, rightUpgrade, overlay, christmas, flip);
|
||||
}
|
||||
|
||||
private List<BakedModel> buildModel(Combination combo) {
|
||||
var mc = Minecraft.getInstance();
|
||||
var modelManager = mc.getItemRenderer().getItemModelShaper().getModelManager();
|
||||
|
||||
var transformation = combo.flip ? flip : identity;
|
||||
var parts = new ArrayList<BakedModel>(4);
|
||||
parts.add(transform(combo.colour() ? colourModel : familyModel, transformation));
|
||||
|
||||
if (combo.overlay() != null) addPart(parts, modelManager, transformation, combo.overlay().model());
|
||||
|
||||
var showChristmas = TurtleOverlay.showElfOverlay(combo.overlay(), combo.christmas());
|
||||
if (showChristmas) addPart(parts, modelManager, transformation, TurtleOverlay.ELF_MODEL);
|
||||
|
||||
addUpgrade(parts, transformation, TurtleSide.LEFT, combo.leftUpgrade());
|
||||
addUpgrade(parts, transformation, TurtleSide.RIGHT, combo.rightUpgrade());
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private void addPart(List<BakedModel> parts, ModelManager modelManager, Transformation transformation, ResourceLocation model) {
|
||||
parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, model), transformation));
|
||||
}
|
||||
|
||||
private void addUpgrade(List<BakedModel> parts, Transformation transformation, TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
|
||||
if (upgrade == null) return;
|
||||
var model = TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side);
|
||||
parts.add(transform(model.model(), transformation.compose(model.matrix())));
|
||||
}
|
||||
|
||||
private BakedModel transform(BakedModel model, Transformation transformation) {
|
||||
if (transformation.equals(Transformation.identity())) return model;
|
||||
return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer);
|
||||
}
|
||||
|
||||
public interface ModelTransformer {
|
||||
BakedModel transform(BakedModel model, Transformation transformation);
|
||||
}
|
||||
}
|
@@ -4,7 +4,6 @@
|
||||
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import dan200.computercraft.client.ClientTableFormatter;
|
||||
import dan200.computercraft.client.gui.AbstractComputerScreen;
|
||||
import dan200.computercraft.client.gui.OptionScreen;
|
||||
@@ -22,11 +21,13 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.JukeboxSong;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.LevelEvent;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
@@ -34,7 +35,6 @@ import java.util.UUID;
|
||||
/**
|
||||
* The client-side implementation of {@link ClientNetworkContext}.
|
||||
*/
|
||||
@AutoService(ClientNetworkContext.class)
|
||||
public final class ClientNetworkContextImpl implements ClientNetworkContext {
|
||||
@Override
|
||||
public void handleChatTable(TableBuilder table) {
|
||||
@@ -62,10 +62,14 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
|
||||
|
||||
@Override
|
||||
public void handlePlayRecord(BlockPos pos, @Nullable Holder<JukeboxSong> song) {
|
||||
var level = Minecraft.getInstance().level;
|
||||
if (level == null) return;
|
||||
|
||||
if (song == null) {
|
||||
Minecraft.getInstance().levelRenderer.stopJukeboxSongAndNotifyNearby(pos);
|
||||
level.levelEvent(LevelEvent.SOUND_STOP_JUKEBOX_SONG, pos, 0);
|
||||
} else {
|
||||
Minecraft.getInstance().levelRenderer.playJukeboxSong(song, pos);
|
||||
var id = level.registryAccess().lookupOrThrow(Registries.JUKEBOX_SONG).getIdOrThrow(song.value());
|
||||
level.levelEvent(LevelEvent.SOUND_PLAY_JUKEBOX_SONG, pos, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.ClientHooks;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.LevelRenderer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.CommonColors;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/**
|
||||
* Utilities for rendering block outline.
|
||||
*
|
||||
* @see ClientHooks#drawHighlight(PoseStack, MultiBufferSource, Camera, BlockHitResult)
|
||||
*/
|
||||
public final class BlockOutlineRenderer {
|
||||
private BlockOutlineRenderer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block outline, handling both normal and high-contrast modes.
|
||||
*
|
||||
* @param bufferSource The buffer source.
|
||||
* @param renderer The function to render a highlight.
|
||||
* @see LevelRenderer#renderBlockOutline(Camera, MultiBufferSource.BufferSource, PoseStack, boolean)
|
||||
*/
|
||||
public static void render(MultiBufferSource bufferSource, Renderer renderer) {
|
||||
var highContrast = Minecraft.getInstance().options.highContrastBlockOutline().get();
|
||||
if (highContrast) renderer.render(bufferSource.getBuffer(RenderType.secondaryBlockOutline()), 0xff000000);
|
||||
|
||||
var colour = highContrast ? CommonColors.HIGH_CONTRAST_DIAMOND : ARGB.color(0x66, CommonColors.BLACK);
|
||||
renderer.render(bufferSource.getBuffer(RenderType.lines()), colour);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Renderer {
|
||||
void render(VertexConsumer buffer, int colour);
|
||||
}
|
||||
}
|
@@ -11,8 +11,7 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
|
||||
import dan200.computercraft.shared.util.WorldUtil;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.client.renderer.ShapeRenderer;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class CableHighlightRenderer {
|
||||
@@ -27,11 +26,10 @@ public final class CableHighlightRenderer {
|
||||
* @param camera The current camera.
|
||||
* @param hit The block hit result for the current player.
|
||||
* @return If we rendered a custom outline.
|
||||
* @see net.minecraft.client.renderer.LevelRenderer#renderHitOutline
|
||||
*/
|
||||
public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
|
||||
var pos = hit.getBlockPos();
|
||||
var world = camera.getEntity().getCommandSenderWorld();
|
||||
var world = camera.getEntity().level();
|
||||
|
||||
var state = world.getBlockState(pos);
|
||||
|
||||
@@ -49,27 +47,9 @@ public final class CableHighlightRenderer {
|
||||
var yOffset = pos.getY() - cameraPos.y();
|
||||
var zOffset = pos.getZ() - cameraPos.z();
|
||||
|
||||
var buffer = bufferSource.getBuffer(RenderType.lines());
|
||||
var matrix4f = transform.last().pose();
|
||||
// TODO: Can we just accesstransformer out LevelRenderer.renderShape?
|
||||
shape.forAllEdges((x1, y1, z1, x2, y2, z2) -> {
|
||||
var xDelta = (float) (x2 - x1);
|
||||
var yDelta = (float) (y2 - y1);
|
||||
var zDelta = (float) (z2 - z1);
|
||||
var len = Mth.sqrt(xDelta * xDelta + yDelta * yDelta + zDelta * zDelta);
|
||||
xDelta = xDelta / len;
|
||||
yDelta = yDelta / len;
|
||||
zDelta = zDelta / len;
|
||||
|
||||
buffer
|
||||
.addVertex(matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset))
|
||||
.setColor(0, 0, 0, 0.4f)
|
||||
.setNormal(transform.last(), xDelta, yDelta, zDelta);
|
||||
buffer
|
||||
.addVertex(matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset))
|
||||
.setColor(0, 0, 0, 0.4f)
|
||||
.setNormal(transform.last(), xDelta, yDelta, zDelta);
|
||||
});
|
||||
BlockOutlineRenderer.render(
|
||||
bufferSource, (buffer, colour) -> ShapeRenderer.renderShape(transform, buffer, shape, xOffset, yOffset, zOffset, colour)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -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,17 +12,17 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.LecternRenderer;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
@@ -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());
|
||||
@@ -71,7 +68,7 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
|
||||
pocketModel.render(
|
||||
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), DyedItemColor.getOrDefault(item, -1),
|
||||
FastColor.ARGB32.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
|
||||
ARGB.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
|
||||
);
|
||||
|
||||
// Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
|
||||
@@ -81,8 +78,8 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
|
||||
// Either render the terminal or a black screen, depending on how close we are.
|
||||
var terminal = computer == null ? null : computer.getTerminal();
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(RenderTypes.TERMINAL));
|
||||
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT));
|
||||
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(camera, POCKET_TERMINAL_RENDER_DISTANCE)) {
|
||||
renderPocketTerminal(poseStack, quadEmitter, terminal);
|
||||
} else {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);
|
||||
|
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Additional render state attached to a {@link ItemFrameRenderState}.
|
||||
*
|
||||
* @see dan200.computercraft.client.ClientHooks#onRenderItemFrame(PoseStack, MultiBufferSource, ItemFrameRenderState, ExtendedItemFrameRenderState, int)
|
||||
*/
|
||||
public class ExtendedItemFrameRenderState {
|
||||
public @Nullable PrintoutData printoutData;
|
||||
public boolean isBook;
|
||||
|
||||
/**
|
||||
* Set up the render state from the {@link ItemFrame}'s {@link ItemStack}.
|
||||
*
|
||||
* @param stack The item frame's item.
|
||||
*/
|
||||
public void setup(ItemStack stack) {
|
||||
if (stack.getItem() instanceof PrintoutItem) {
|
||||
printoutData = PrintoutData.getOrEmpty(stack);
|
||||
isBook = stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get();
|
||||
} else {
|
||||
printoutData = null;
|
||||
isBook = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,98 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.model.turtle.ModelTransformer;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.entity.ItemRenderer;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Vector4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utilities for rendering {@link BakedModel}s and {@link BakedQuad}s.
|
||||
*/
|
||||
public final class ModelRenderer {
|
||||
private ModelRenderer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of {@linkplain BakedQuad quads} to a buffer.
|
||||
* <p>
|
||||
* This is not intended to be used directly, but instead by {@link ClientPlatformHelper#renderBakedModel(PoseStack, MultiBufferSource, BakedModel, int, int, int[])}. The
|
||||
* implementation here is pretty similar to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, ItemStack, int, int)},
|
||||
* but supports inverted quads (i.e. those with a negative scale).
|
||||
*
|
||||
* @param transform The current matrix transformation to apply.
|
||||
* @param buffer The buffer to draw to.
|
||||
* @param quads The quads to draw.
|
||||
* @param lightmapCoord The current packed lightmap coordinate.
|
||||
* @param overlayLight The current overlay light.
|
||||
* @param tints Block colour tints to apply to the model.
|
||||
*/
|
||||
public static void renderQuads(PoseStack transform, VertexConsumer buffer, List<BakedQuad> quads, int lightmapCoord, int overlayLight, int @Nullable [] tints) {
|
||||
var matrix = transform.last();
|
||||
var inverted = matrix.pose().determinant() < 0;
|
||||
|
||||
for (var bakedquad : quads) {
|
||||
var tint = -1;
|
||||
if (tints != null && bakedquad.isTinted()) {
|
||||
var idx = bakedquad.getTintIndex();
|
||||
if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
|
||||
}
|
||||
|
||||
putBulkQuad(buffer, matrix, bakedquad, tint, lightmapCoord, overlayLight, inverted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, float, int, int)} which
|
||||
* will reverse vertex order when the matrix is inverted.
|
||||
*
|
||||
* @param buffer The buffer to draw to.
|
||||
* @param pose The current matrix stack.
|
||||
* @param quad The quad to draw.
|
||||
* @param colour The tint for this quad.
|
||||
* @param lightmapCoord The lightmap coordinate
|
||||
* @param overlayLight The overlay light.
|
||||
* @param invert Whether to reverse the order of this quad.
|
||||
*/
|
||||
private static void putBulkQuad(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, int colour, int lightmapCoord, int overlayLight, boolean invert) {
|
||||
var matrix = pose.pose();
|
||||
// It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
|
||||
// Direction.rotate (so not out of nowhere!), but is a little suspicious.
|
||||
var dirNormal = quad.getDirection().getNormal();
|
||||
var vector = new Vector4f();
|
||||
|
||||
matrix.transform(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f, vector).normalize();
|
||||
float normalX = vector.x(), normalY = vector.y(), normalZ = vector.z();
|
||||
|
||||
var vertices = quad.getVertices();
|
||||
for (var vertex = 0; vertex < 4; vertex++) {
|
||||
var i = ModelTransformer.getVertexOffset(vertex, invert);
|
||||
|
||||
var x = Float.intBitsToFloat(vertices[i]);
|
||||
var y = Float.intBitsToFloat(vertices[i + 1]);
|
||||
var z = Float.intBitsToFloat(vertices[i + 2]);
|
||||
|
||||
matrix.transform(x, y, z, 1, vector);
|
||||
|
||||
var u = Float.intBitsToFloat(vertices[i + 4]);
|
||||
var v = Float.intBitsToFloat(vertices[i + 5]);
|
||||
buffer.addVertex(
|
||||
vector.x(), vector.y(), vector.z(),
|
||||
colour, u, v, overlayLight, lightmapCoord,
|
||||
normalX, normalY, normalZ
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,13 +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.util.FastColor;
|
||||
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;
|
||||
|
||||
@@ -29,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() {
|
||||
}
|
||||
|
||||
@@ -72,7 +81,7 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
|
||||
renderLight(transform, bufferSource, lightColour, width, height);
|
||||
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT));
|
||||
if (terminal == null) {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
|
||||
} else {
|
||||
@@ -83,22 +92,89 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
}
|
||||
|
||||
private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
|
||||
var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
|
||||
var textures = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
|
||||
var spriteRenderer = new SpriteRenderer(transform, render, 0, light, colour);
|
||||
renderBorder(spriteRenderer, textures, width, height);
|
||||
}
|
||||
|
||||
var r = (colour >>> 16) & 0xFF;
|
||||
var g = (colour >>> 8) & 0xFF;
|
||||
var b = colour & 0xFF;
|
||||
private static void renderBorder(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int width, int height) {
|
||||
var sprites = Minecraft.getInstance().getGuiSprites();
|
||||
|
||||
var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b);
|
||||
ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true);
|
||||
// Find our border, forcing it to be a nine-sliced texture.
|
||||
var borderSprite = sprites.getSprite(textures.border());
|
||||
var borderSlice = getSlice(sprites.getSpriteScaling(borderSprite), DEFAULT_BORDER);
|
||||
var borderBounds = borderSlice.border();
|
||||
|
||||
// And take the separate bottom bit of the pocket computer.
|
||||
var bottomTexture = textures.pocketBottom();
|
||||
if (bottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
|
||||
var bottomSprite = sprites.getSprite(bottomTexture);
|
||||
var bottomSlice = getSlice(sprites.getSpriteScaling(bottomSprite), DEFAULT_BOTTOM);
|
||||
var bottomBounds = bottomSlice.border();
|
||||
|
||||
// Now draw a nine-sliced texture, by stitching together the top parts of the border with the pocket bottom.
|
||||
|
||||
// Top bar
|
||||
renderer.blit(
|
||||
borderSprite, -borderBounds.left(), -borderBounds.top(), borderBounds.left(), borderBounds.top(),
|
||||
0, 0, borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
renderer.blitTiled(
|
||||
borderSprite, 0, -borderBounds.top(), width, borderBounds.top(),
|
||||
borderBounds.left(), 0, borderSlice.width() - borderBounds.left() - borderBounds.right(), borderBounds.top(),
|
||||
borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
renderer.blit(
|
||||
borderSprite, width, -borderBounds.top(), borderBounds.right(), borderBounds.top(),
|
||||
borderSlice.width() - borderBounds.right(), 0, borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
|
||||
// Vertical bars
|
||||
renderer.blitTiled(
|
||||
borderSprite, -borderBounds.left(), 0, borderBounds.left(), height,
|
||||
0, borderBounds.top(), borderBounds.left(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(),
|
||||
borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
renderer.blitTiled(
|
||||
borderSprite, width, 0, borderBounds.right(), height,
|
||||
borderSlice.width() - borderBounds.right(), borderBounds.top(), borderBounds.right(), borderSlice.height() - borderBounds.top() - borderBounds.bottom(),
|
||||
borderSlice.width(), borderSlice.height()
|
||||
);
|
||||
|
||||
// Bottom
|
||||
renderer.blit(
|
||||
bottomSprite, -bottomBounds.left(), height, bottomBounds.left(), bottomSlice.height(),
|
||||
0, 0, bottomSlice.width(), bottomSlice.height()
|
||||
);
|
||||
renderer.blitTiled(
|
||||
bottomSprite, 0, height, width, bottomSlice.height(),
|
||||
bottomBounds.left(), 0, bottomSlice.width() - bottomBounds.left() - bottomBounds.right(), bottomSlice.height(),
|
||||
bottomSlice.width(), bottomSlice.height()
|
||||
);
|
||||
renderer.blit(
|
||||
bottomSprite, width, height, bottomBounds.right(), bottomSlice.height(),
|
||||
bottomSlice.width() - bottomBounds.right(), 0, bottomSlice.width(), bottomSlice.height()
|
||||
);
|
||||
}
|
||||
|
||||
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
|
||||
var buffer = render.getBuffer(RenderTypes.TERMINAL);
|
||||
var buffer = render.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
|
||||
FixedWidthFontRenderer.drawQuad(
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, buffer),
|
||||
width - LIGHT_HEIGHT * 2, height + BORDER / 2.0f, 0.001f, LIGHT_HEIGHT * 2, LIGHT_HEIGHT,
|
||||
FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP
|
||||
ARGB.opaque(colour), LightTexture.FULL_BRIGHT
|
||||
);
|
||||
}
|
||||
|
||||
private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice(
|
||||
36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12), 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;
|
||||
}
|
||||
}
|
||||
|
@@ -8,10 +8,8 @@ import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.decoration.ItemFrame;
|
||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
|
||||
@@ -35,25 +33,22 @@ public final class PrintoutItemRenderer extends ItemMapLikeRenderer {
|
||||
transform.scale(0.42f, 0.42f, -0.42f);
|
||||
transform.translate(-0.5f, -0.48f, 0.0f);
|
||||
|
||||
drawPrintout(transform, render, stack, light);
|
||||
drawPrintout(transform, render, PrintoutData.getOrEmpty(stack), stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get(), light);
|
||||
}
|
||||
|
||||
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int packedLight) {
|
||||
public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, PrintoutData data, boolean isBook, int packedLight) {
|
||||
// Move a little bit forward to ensure we're not clipping with the frame
|
||||
transform.translate(0.0f, 0.0f, -0.001f);
|
||||
transform.mulPose(Axis.ZP.rotationDegrees(180f));
|
||||
transform.scale(0.95f, 0.95f, -0.95f);
|
||||
transform.translate(-0.5f, -0.5f, 0.0f);
|
||||
|
||||
var light = frame.getType() == EntityType.GLOW_ITEM_FRAME ? 0xf000d2 : packedLight; // See getLightVal.
|
||||
drawPrintout(transform, render, stack, light);
|
||||
var light = frame.isGlowFrame ? 0xf000d2 : packedLight; // See getLightCoords.
|
||||
drawPrintout(transform, render, data, isBook, light);
|
||||
}
|
||||
|
||||
private static void drawPrintout(PoseStack transform, MultiBufferSource render, ItemStack stack, int light) {
|
||||
var pageData = stack.getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
|
||||
|
||||
private static void drawPrintout(PoseStack transform, MultiBufferSource render, PrintoutData pageData, boolean book, int light) {
|
||||
var pages = pageData.pages();
|
||||
var book = ((PrintoutItem) stack.getItem()).getType() == PrintoutItem.Type.BOOK;
|
||||
|
||||
double width = LINE_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2;
|
||||
double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2;
|
||||
|
@@ -11,6 +11,8 @@ import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.List;
|
||||
@@ -23,6 +25,12 @@ import static dan200.computercraft.shared.media.items.PrintoutData.LINES_PER_PAG
|
||||
* {@linkplain PrintoutItemRenderer in-hand/item frame printouts}.
|
||||
*/
|
||||
public final class PrintoutRenderer {
|
||||
/**
|
||||
* Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
|
||||
* it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
|
||||
*/
|
||||
private static final RenderType BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
|
||||
|
||||
private static final float BG_SIZE = 256.0f;
|
||||
|
||||
/**
|
||||
@@ -58,11 +66,14 @@ public final class PrintoutRenderer {
|
||||
private static final int COVER_Y = Y_SIZE;
|
||||
private static final int COVER_X = X_SIZE + 4 * X_FOLD_SIZE;
|
||||
|
||||
private static final float BOOK_Z_OFFSET = -0.1f;
|
||||
private static final float PAGE_Z_OFFSET = -0.01f;
|
||||
|
||||
private PrintoutRenderer() {
|
||||
}
|
||||
|
||||
public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, TextBuffer[] text, TextBuffer[] colours) {
|
||||
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
|
||||
var buffer = bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
|
||||
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
|
||||
for (var line = 0; line < LINES_PER_PAGE && line < text.length; line++) {
|
||||
FixedWidthFontRenderer.drawString(emitter,
|
||||
@@ -73,7 +84,7 @@ public final class PrintoutRenderer {
|
||||
}
|
||||
|
||||
public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, List<PrintoutData.Line> lines) {
|
||||
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
|
||||
var buffer = bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
|
||||
var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
|
||||
for (var line = 0; line < LINES_PER_PAGE && line < lines.size(); line++) {
|
||||
var lineContents = lines.get(start + line);
|
||||
@@ -90,7 +101,7 @@ public final class PrintoutRenderer {
|
||||
var leftPages = page;
|
||||
var rightPages = pages - page - 1;
|
||||
|
||||
var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_BACKGROUND);
|
||||
var buffer = bufferSource.getBuffer(BACKGROUND);
|
||||
|
||||
if (isBook) {
|
||||
// Border
|
||||
@@ -99,12 +110,12 @@ public final class PrintoutRenderer {
|
||||
var right = x + X_SIZE + offset - 4;
|
||||
|
||||
// Left and right border
|
||||
drawTexture(matrix, buffer, left - 4, y - 8, z - 0.02f, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
|
||||
drawTexture(matrix, buffer, right, y - 8, z - 0.02f, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
|
||||
drawTexture(matrix, buffer, left - 4, y - 8, z + BOOK_Z_OFFSET, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
|
||||
drawTexture(matrix, buffer, right, y - 8, z + BOOK_Z_OFFSET, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
|
||||
|
||||
// Draw centre panel (just stretched texture, sorry).
|
||||
drawTexture(matrix, buffer,
|
||||
x - offset, y, z - 0.02f, X_SIZE + offset * 2, Y_SIZE,
|
||||
x - offset, y, z + BOOK_Z_OFFSET, X_SIZE + offset * 2, Y_SIZE,
|
||||
COVER_X + COVER_SIZE / 2.0f, COVER_SIZE, COVER_SIZE, Y_SIZE,
|
||||
light
|
||||
);
|
||||
@@ -112,20 +123,20 @@ public final class PrintoutRenderer {
|
||||
var borderX = left;
|
||||
while (borderX < right) {
|
||||
double thisWidth = Math.min(right - borderX, X_SIZE);
|
||||
drawTexture(matrix, buffer, borderX, y - 8, z - 0.02f, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
|
||||
drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z - 0.02f, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
|
||||
drawTexture(matrix, buffer, borderX, y - 8, z + BOOK_Z_OFFSET, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
|
||||
drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z + BOOK_Z_OFFSET, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
|
||||
borderX = (float) (borderX + thisWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Current page background: Z-offset is interleaved between the "zeroth" left/right page and the first
|
||||
// left/right page, so that the "bold" border can be drawn over the edge where appropriate.
|
||||
drawTexture(matrix, buffer, x, y, z - 1e-3f * 0.5f, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
|
||||
drawTexture(matrix, buffer, x, y, z + PAGE_Z_OFFSET, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
|
||||
|
||||
// Left pages
|
||||
for (var n = 0; n <= leftPages; n++) {
|
||||
drawTexture(matrix, buffer,
|
||||
x - offsetAt(n), y, z - 1e-3f * n,
|
||||
x - offsetAt(n), y, z + PAGE_Z_OFFSET * (n + 1),
|
||||
// Use the left "bold" fold for the outermost page
|
||||
n == leftPages ? 0 : X_FOLD_SIZE, 0,
|
||||
X_FOLD_SIZE, Y_SIZE, light
|
||||
@@ -135,7 +146,7 @@ public final class PrintoutRenderer {
|
||||
// Right pages
|
||||
for (var n = 0; n <= rightPages; n++) {
|
||||
drawTexture(matrix, buffer,
|
||||
x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z - 1e-3f * n,
|
||||
x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z + PAGE_Z_OFFSET * (n + 1),
|
||||
// Two folds, then the main page. Use the right "bold" fold for the outermost page.
|
||||
X_FOLD_SIZE * 2 + X_SIZE + (n == rightPages ? X_FOLD_SIZE : 0), 0,
|
||||
X_FOLD_SIZE, Y_SIZE, light
|
||||
|
@@ -1,102 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.RenderStateShard;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Shared {@link RenderType}s used throughout the mod.
|
||||
*/
|
||||
public class RenderTypes {
|
||||
public static final int FULL_BRIGHT_LIGHTMAP = (0xF << 4) | (0xF << 20);
|
||||
|
||||
private static @Nullable MonitorTextureBufferShader monitorTboShader;
|
||||
|
||||
/**
|
||||
* Renders a fullbright terminal.
|
||||
*/
|
||||
public static final RenderType TERMINAL = RenderType.text(FixedWidthFontRenderer.FONT);
|
||||
|
||||
/**
|
||||
* Renders a monitor with the TBO shader.
|
||||
*
|
||||
* @see MonitorTextureBufferShader
|
||||
*/
|
||||
public static final RenderType MONITOR_TBO = Types.MONITOR_TBO;
|
||||
|
||||
/**
|
||||
* A variant of {@link #TERMINAL} which uses the lightmap rather than rendering fullbright.
|
||||
*/
|
||||
public static final RenderType PRINTOUT_TEXT = RenderType.text(FixedWidthFontRenderer.FONT);
|
||||
|
||||
/**
|
||||
* Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
|
||||
* it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
|
||||
*/
|
||||
public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
|
||||
|
||||
/**
|
||||
* Render type for {@linkplain GuiSprites GUI sprites}.
|
||||
*/
|
||||
public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE);
|
||||
|
||||
public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
|
||||
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
|
||||
return monitorTboShader;
|
||||
}
|
||||
|
||||
public static ShaderInstance getTerminalShader() {
|
||||
return Objects.requireNonNull(GameRenderer.getRendertypeTextShader(), "Text shader has not been registered");
|
||||
}
|
||||
|
||||
public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
|
||||
load.accept(
|
||||
new MonitorTextureBufferShader(
|
||||
resources,
|
||||
ComputerCraftAPI.MOD_ID + "/monitor_tbo",
|
||||
MONITOR_TBO.format()
|
||||
),
|
||||
x -> monitorTboShader = (MonitorTextureBufferShader) x
|
||||
);
|
||||
}
|
||||
|
||||
private static final class Types extends RenderType {
|
||||
private static final RenderStateShard.TextureStateShard TERM_FONT_TEXTURE = new TextureStateShard(
|
||||
FixedWidthFontRenderer.FONT,
|
||||
false, false // blur, minimap
|
||||
);
|
||||
|
||||
static final RenderType MONITOR_TBO = RenderType.create(
|
||||
"monitor_tbo", DefaultVertexFormat.POSITION_TEX, VertexFormat.Mode.TRIANGLE_STRIP, 128,
|
||||
false, false, // useDelegate, needsSorting
|
||||
RenderType.CompositeState.builder()
|
||||
.setTextureState(TERM_FONT_TEXTURE)
|
||||
.setShaderState(new ShaderStateShard(RenderTypes::getMonitorTextureBufferShader))
|
||||
.createCompositeState(false)
|
||||
);
|
||||
|
||||
@SuppressWarnings("UnusedMethod")
|
||||
private Types(String name, VertexFormat format, VertexFormat.Mode mode, int buffer, boolean crumbling, boolean sort, Runnable setup, Runnable teardown) {
|
||||
super(name, format, mode, buffer, crumbling, sort, setup, teardown);
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,129 +6,70 @@ package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link GuiGraphics}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity
|
||||
* renderer).
|
||||
* A {@link GuiGraphics}-equivalent renders to a {@link VertexConsumer}. This is suitable for rendering outside of a
|
||||
* GUI, such as part of an entity renderer.
|
||||
* <p>
|
||||
* This batches all render calls together, though requires that all {@link TextureAtlasSprite}s are on the same sprite
|
||||
* sheet.
|
||||
*/
|
||||
public class SpriteRenderer {
|
||||
public static final ResourceLocation TEXTURE = ResourceLocation.withDefaultNamespace("textures/atlas/gui.png");
|
||||
|
||||
private final Matrix4f transform;
|
||||
private final VertexConsumer builder;
|
||||
private final MultiBufferSource buffers;
|
||||
private final int light;
|
||||
private final int z;
|
||||
private final int r, g, b;
|
||||
private final int colour;
|
||||
|
||||
public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) {
|
||||
public SpriteRenderer(Matrix4f transform, MultiBufferSource buffers, int z, int light, int colour) {
|
||||
this.transform = transform;
|
||||
this.builder = builder;
|
||||
this.buffers = buffers;
|
||||
this.z = z;
|
||||
this.light = light;
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.colour = colour;
|
||||
}
|
||||
|
||||
public static SpriteRenderer createForGui(GuiGraphics graphics, RenderType renderType) {
|
||||
return new SpriteRenderer(
|
||||
graphics.pose().last().pose(), graphics.bufferSource().getBuffer(renderType),
|
||||
0, RenderTypes.FULL_BRIGHT_LIGHTMAP, 255, 255, 255
|
||||
);
|
||||
public void blit(TextureAtlasSprite sprite, int x0, int y0, int width, int height, int spriteX, int spriteY, int spriteWidth, int spriteHeight) {
|
||||
if (width == 0 || height == 0) return;
|
||||
|
||||
var x1 = x0 + width;
|
||||
var y1 = y0 + height;
|
||||
var u0 = sprite.getU((float) spriteX / spriteWidth);
|
||||
var u1 = sprite.getU((float) (spriteX + width) / spriteWidth);
|
||||
var v0 = sprite.getV((float) spriteY / spriteHeight);
|
||||
var v1 = sprite.getV((float) (spriteY + height) / spriteHeight);
|
||||
|
||||
var vertices = buffers.getBuffer(RenderType.text(sprite.atlasLocation()));
|
||||
vertices.addVertex(transform, x0, y1, z).setColor(colour).setUv(u0, v1).setLight(light);
|
||||
vertices.addVertex(transform, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
|
||||
vertices.addVertex(transform, x1, y0, z).setColor(colour).setUv(u1, v0).setLight(light);
|
||||
vertices.addVertex(transform, x0, y0, z).setColor(colour).setUv(u0, v0).setLight(light);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single sprite.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
*/
|
||||
public void blit(TextureAtlasSprite sprite, int x, int y, int width, int height) {
|
||||
blit(x, y, width, height, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
public void blitTiled(
|
||||
TextureAtlasSprite sprite,
|
||||
int x, int y, int width, int height,
|
||||
int tileX, int tileY, int tileWidth, int tileHeight, int spriteWidth, int spriteHeight
|
||||
) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
if (tileWidth <= 0 || tileHeight <= 0) {
|
||||
throw new IllegalArgumentException("Tiled sprite texture size must be positive, got " + tileWidth + "x" + tileHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiGraphics#blitNineSliced},
|
||||
* the middle texture is stretched rather than repeated.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param leftBorder The width of the left border.
|
||||
* @param rightBorder The width of the right border.
|
||||
* @param textureWidth The width of the whole texture.
|
||||
*/
|
||||
public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) {
|
||||
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
|
||||
if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders");
|
||||
|
||||
var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth);
|
||||
var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth);
|
||||
|
||||
blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1());
|
||||
blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1());
|
||||
blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiGraphics#blitNineSliced},
|
||||
* the middle texture is stretched rather than repeated.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param topBorder The height of the top border.
|
||||
* @param bottomBorder The height of the bottom border.
|
||||
* @param textureHeight The height of the whole texture.
|
||||
*/
|
||||
public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) {
|
||||
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
|
||||
if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders");
|
||||
|
||||
var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight);
|
||||
var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight);
|
||||
|
||||
blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart);
|
||||
blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd);
|
||||
blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite.
|
||||
*
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param u0 The first U coordinate.
|
||||
* @param v0 The first V coordinate.
|
||||
* @param u1 The second U coordinate.
|
||||
* @param v1 The second V coordinate.
|
||||
*/
|
||||
public void blit(
|
||||
int x, int y, int width, int height, float u0, float v0, float u1, float v1) {
|
||||
builder.addVertex(transform, x, y + height, z).setColor(r, g, b, 255).setUv(u0, v1).setLight(light);
|
||||
builder.addVertex(transform, x + width, y + height, z).setColor(r, g, b, 255).setUv(u1, v1).setLight(light);
|
||||
builder.addVertex(transform, x + width, y, z).setColor(r, g, b, 255).setUv(u1, v0).setLight(light);
|
||||
builder.addVertex(transform, x, y, z).setColor(r, g, b, 255).setUv(u0, v0).setLight(light);
|
||||
}
|
||||
|
||||
public static float u(TextureAtlasSprite sprite, int x, int width) {
|
||||
return sprite.getU((float) x / width);
|
||||
}
|
||||
|
||||
public static float v(TextureAtlasSprite sprite, int y, int height) {
|
||||
return sprite.getV((float) y / height);
|
||||
for (var xOffset = 0; xOffset < width; xOffset += tileWidth) {
|
||||
var sliceWidth = Math.min(tileWidth, width - xOffset);
|
||||
for (var yOffset = 0; yOffset < height; yOffset += tileHeight) {
|
||||
var sliceHeight = Math.min(tileHeight, height - yOffset);
|
||||
blit(sprite, x + xOffset, y + yOffset, sliceWidth, sliceHeight, tileX, tileY, spriteWidth, spriteHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,12 +6,13 @@ package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.ClientRegistry;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.client.turtle.TurtleOverlayManager;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -20,15 +21,17 @@ import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.CommonColors;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
|
||||
public static final ResourceLocation NORMAL_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_normal");
|
||||
public static final ResourceLocation ADVANCED_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_advanced");
|
||||
public static final ResourceLocation COLOUR_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
|
||||
|
||||
private final BlockEntityRenderDispatcher renderer;
|
||||
@@ -40,7 +43,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight) {
|
||||
public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, Vec3 camera) {
|
||||
transform.pushPose();
|
||||
|
||||
// Translate the turtle first, so the label moves with it.
|
||||
@@ -62,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();
|
||||
}
|
||||
@@ -72,31 +75,24 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
transform.translate(0.5f, 0.5f, 0.5f);
|
||||
var yaw = turtle.getRenderYaw(partialTicks);
|
||||
transform.mulPose(Axis.YP.rotationDegrees(180.0f - yaw));
|
||||
if (label != null && (label.equals("Dinnerbone") || label.equals("Grumm"))) {
|
||||
transform.scale(1.0f, -1.0f, 1.0f);
|
||||
}
|
||||
transform.translate(-0.5f, -0.5f, -0.5f);
|
||||
|
||||
// Render the turtle
|
||||
var colour = turtle.getColour();
|
||||
var overlay = turtle.getOverlay();
|
||||
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
|
||||
|
||||
if (colour == -1) {
|
||||
// Render the turtle using its item model.
|
||||
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper();
|
||||
var model = modelManager.getItemModel(turtle.getBlockState().getBlock().asItem());
|
||||
if (model == null) model = modelManager.getModelManager().getMissingModel();
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, model, null);
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
|
||||
} else {
|
||||
// Otherwise render it using the colour item.
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ FastColor.ARGB32.opaque(colour) });
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ ARGB.opaque(colour) });
|
||||
}
|
||||
|
||||
// Render the overlay
|
||||
if (overlay != null) renderModel(transform, buffers, lightmapCoord, overlayLight, overlay.model(), null);
|
||||
if (overlay != null) overlay.model().render(transform, buffers, lightmapCoord, overlayLight);
|
||||
|
||||
// And the Christmas overlay.
|
||||
var showChristmas = TurtleOverlay.showElfOverlay(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
|
||||
var showChristmas = Holiday.getCurrent() == Holiday.CHRISTMAS && (overlay == null || overlay.showElfOverlay());
|
||||
if (showChristmas) renderModel(transform, buffers, lightmapCoord, overlayLight, TurtleOverlay.ELF_MODEL, null);
|
||||
|
||||
// Render the upgrades
|
||||
@@ -107,7 +103,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
}
|
||||
|
||||
private void renderUpgrade(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
|
||||
var upgrade = turtle.getUpgrade(side);
|
||||
var upgrade = turtle.getAccess().getUpgradeWithData(side);
|
||||
if (upgrade == null) return;
|
||||
transform.pushPose();
|
||||
|
||||
@@ -116,42 +112,15 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
|
||||
transform.translate(0.0f, -0.5f, -0.5f);
|
||||
|
||||
var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
|
||||
applyTransformation(transform, model.matrix());
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
|
||||
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
|
||||
.renderForLevel(upgrade, side, turtle.getAccess(), transform, buffers, lightmapCoord, overlayLight);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, int @Nullable [] tints) {
|
||||
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getModelManager();
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
|
||||
var modelManager = Minecraft.getInstance().getModelManager();
|
||||
ClientRegistry.getModel(modelManager, modelLocation).render(transform, buffers, lightmapCoord, overlayLight, tints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block model.
|
||||
*
|
||||
* @param transform The current matrix stack.
|
||||
* @param renderer The buffer to write to.
|
||||
* @param lightmapCoord The current lightmap coordinate.
|
||||
* @param overlayLight The overlay light.
|
||||
* @param model The model to render.
|
||||
* @param tints Tints for the quads, as an array of RGB values.
|
||||
* @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel
|
||||
*/
|
||||
private void renderModel(PoseStack transform, MultiBufferSource renderer, int lightmapCoord, int overlayLight, BakedModel model, int @Nullable [] tints) {
|
||||
ClientPlatformHelper.get().renderBakedModel(transform, renderer, model, lightmapCoord, overlayLight, tints);
|
||||
}
|
||||
|
||||
private static void applyTransformation(PoseStack stack, Transformation transformation) {
|
||||
var trans = transformation.getTranslation();
|
||||
stack.translate(trans.x(), trans.y(), trans.z());
|
||||
|
||||
stack.mulPose(transformation.getLeftRotation());
|
||||
|
||||
var scale = transformation.getScale();
|
||||
stack.scale(scale.x(), scale.y(), scale.z());
|
||||
|
||||
stack.mulPose(transformation.getRightRotation());
|
||||
}
|
||||
}
|
||||
|
@@ -4,45 +4,41 @@
|
||||
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.buffers.GpuBuffer;
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import com.mojang.blaze3d.vertex.VertexBuffer;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.annotations.ForgeOverride;
|
||||
import dan200.computercraft.client.FrameInfo;
|
||||
import dan200.computercraft.client.integration.ShaderMod;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.vbo.DirectBuffers;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.fog.FogRenderer;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBlockEntity> {
|
||||
/**
|
||||
@@ -53,13 +49,11 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
|
||||
private static @Nullable ByteBuffer backingBuffer;
|
||||
|
||||
private static long lastFrame = -1;
|
||||
|
||||
public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight) {
|
||||
public void render(MonitorBlockEntity monitor, float partialTicks, PoseStack transform, MultiBufferSource bufferSource, int lightmapCoord, int overlayLight, Vec3 camera) {
|
||||
// Render from the origin monitor
|
||||
var originTerminal = monitor.getOriginClientMonitor();
|
||||
if (originTerminal == null) return;
|
||||
@@ -76,7 +70,6 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
return;
|
||||
}
|
||||
|
||||
lastFrame = renderFrame;
|
||||
renderState.lastRenderFrame = renderFrame;
|
||||
renderState.lastRenderPos = monitorPos;
|
||||
|
||||
@@ -124,7 +117,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
transform.popPose();
|
||||
} else {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
|
||||
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
|
||||
-MARGIN, MARGIN,
|
||||
(float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2)
|
||||
);
|
||||
@@ -136,126 +129,137 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
private static void renderTerminal(
|
||||
Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin
|
||||
) {
|
||||
int width = terminal.getWidth(), height = terminal.getHeight();
|
||||
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
|
||||
|
||||
var renderType = currentRenderer();
|
||||
var redraw = monitor.pollTerminalChanged();
|
||||
if (renderState.createBuffer(renderType)) redraw = true;
|
||||
if (renderState.vertexBuffer == null) redraw = true;
|
||||
|
||||
switch (renderType) {
|
||||
case TBO -> {
|
||||
if (redraw) {
|
||||
var terminalBuffer = getBuffer(width * height * 3);
|
||||
MonitorTextureBufferShader.setTerminalData(terminalBuffer, terminal);
|
||||
DirectBuffers.setBufferData(GL31.GL_TEXTURE_BUFFER, renderState.tboBuffer, terminalBuffer, GL20.GL_STATIC_DRAW);
|
||||
if (redraw) {
|
||||
// Cursor, Foreground, Background+Margin
|
||||
var maxQuadCount = 1 + (terminal.getWidth() * terminal.getHeight()) + ((terminal.getWidth() + 2) * (terminal.getHeight() + 2));
|
||||
var maxVertexCount = 4 * maxQuadCount;
|
||||
var sink = ShaderMod.get().getQuadEmitter(maxQuadCount, MonitorBlockEntityRenderer::getBuffer);
|
||||
|
||||
var uniformBuffer = getBuffer(MonitorTextureBufferShader.UNIFORM_SIZE);
|
||||
MonitorTextureBufferShader.setUniformData(uniformBuffer, terminal);
|
||||
DirectBuffers.setBufferData(GL31.GL_UNIFORM_BUFFER, renderState.tboUniform, uniformBuffer, GL20.GL_STATIC_DRAW);
|
||||
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin);
|
||||
var vertexCountAfterBackground = sink.vertexCount();
|
||||
|
||||
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
|
||||
var vertexCountAfterForeground = sink.vertexCount();
|
||||
|
||||
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
|
||||
var vertexCountAfterCursor = sink.vertexCount();
|
||||
|
||||
if (vertexCountAfterCursor > maxVertexCount) {
|
||||
throw new IllegalStateException("Drew too many vertices. Expected " + maxVertexCount + ", drew " + vertexCountAfterCursor);
|
||||
}
|
||||
|
||||
if (vertexCountAfterCursor != 0) {
|
||||
renderState.register();
|
||||
|
||||
var commandEncoder = RenderSystem.getDevice().createCommandEncoder();
|
||||
|
||||
var resultBuffer = sink.byteBuffer().flip();
|
||||
|
||||
// Ensure our buffer contains the correct number of vertices.
|
||||
if (resultBuffer.remaining() != sink.format().getVertexSize() * vertexCountAfterCursor) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Mismatched vertex count. Buffer is %d bytes long, but was expected to be %d (vertex size) * %d (vertex count) = %d bytes.",
|
||||
resultBuffer.limit(), sink.format().getVertexSize(), vertexCountAfterCursor, sink.format().getVertexSize() * vertexCountAfterCursor
|
||||
));
|
||||
}
|
||||
|
||||
// Nobody knows what they're doing!
|
||||
var active = GlStateManager._getActiveTexture();
|
||||
RenderSystem.activeTexture(MonitorTextureBufferShader.TEXTURE_INDEX);
|
||||
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, renderState.tboTexture);
|
||||
RenderSystem.activeTexture(active);
|
||||
|
||||
var shader = RenderTypes.getMonitorTextureBufferShader();
|
||||
shader.setupUniform(renderState.tboUniform);
|
||||
|
||||
var buffer = Tesselator.getInstance().begin(RenderTypes.MONITOR_TBO.mode(), RenderTypes.MONITOR_TBO.format());
|
||||
tboVertex(buffer, matrix, -xMargin, -yMargin);
|
||||
tboVertex(buffer, matrix, -xMargin, pixelHeight + yMargin);
|
||||
tboVertex(buffer, matrix, pixelWidth + xMargin, -yMargin);
|
||||
tboVertex(buffer, matrix, pixelWidth + xMargin, pixelHeight + yMargin);
|
||||
RenderTypes.MONITOR_TBO.draw(Nullability.assertNonNull(buffer.build()));
|
||||
}
|
||||
case VBO -> {
|
||||
var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
|
||||
var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
|
||||
if (redraw) {
|
||||
var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
|
||||
|
||||
// In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
|
||||
// a starting and ending offset, and so need to use two buffers instead.
|
||||
|
||||
renderToBuffer(backgroundBuffer, size, sink ->
|
||||
DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
|
||||
|
||||
renderToBuffer(foregroundBuffer, size, sink -> {
|
||||
DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
|
||||
// If the cursor is visible, we append it to the end of our buffer. When rendering, we can either
|
||||
// render n or n+1 quads and so toggle the cursor on and off.
|
||||
DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
|
||||
});
|
||||
// Upload the buffer, reallocating if required.
|
||||
if (renderState.vertexBuffer == null || resultBuffer.remaining() > renderState.vertexBuffer.size()) {
|
||||
if (renderState.vertexBuffer != null) {
|
||||
renderState.vertexBuffer.close();
|
||||
renderState.vertexBuffer = null;
|
||||
}
|
||||
renderState.vertexBuffer = RenderSystem.getDevice().createBuffer(
|
||||
() -> "Monitor at " + monitor.getOrigin().getBlockPos(), GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, resultBuffer
|
||||
);
|
||||
} else if (!renderState.vertexBuffer.isClosed()) {
|
||||
commandEncoder.writeToBuffer(renderState.vertexBuffer.slice(), resultBuffer);
|
||||
}
|
||||
|
||||
// Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will
|
||||
// use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the
|
||||
// normal render distance (~200), and the edges of the monitor fade out due to fog.
|
||||
// There's not really a good way around this, at least without using a custom render type (which the VBO
|
||||
// renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
|
||||
// absurdly high value.
|
||||
var oldFogStart = RenderSystem.getShaderFogStart();
|
||||
RenderSystem.setShaderFogStart(1e4f);
|
||||
|
||||
RenderTypes.TERMINAL.setupRenderState();
|
||||
|
||||
// Compose the existing model view matrix with our transformation matrix.
|
||||
var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix);
|
||||
|
||||
// Render background geometry
|
||||
backgroundBuffer.bind();
|
||||
backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader());
|
||||
|
||||
// Render foreground geometry with glPolygonOffset enabled.
|
||||
RenderSystem.polygonOffset(-1.0f, -10.0f);
|
||||
RenderSystem.enablePolygonOffset();
|
||||
|
||||
foregroundBuffer.bind();
|
||||
foregroundBuffer.drawWithShader(
|
||||
modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader(),
|
||||
// Skip the cursor quad if it is not visible this frame.
|
||||
FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
|
||||
? foregroundBuffer.getIndexCount() - RenderTypes.TERMINAL.mode().indexCount(4)
|
||||
: foregroundBuffer.getIndexCount()
|
||||
);
|
||||
|
||||
// Clear state
|
||||
RenderSystem.polygonOffset(0.0f, -0.0f);
|
||||
RenderSystem.disablePolygonOffset();
|
||||
RenderTypes.TERMINAL.clearRenderState();
|
||||
VertexBuffer.unbind();
|
||||
|
||||
RenderSystem.setShaderFogStart(oldFogStart);
|
||||
}
|
||||
case BEST -> throw new IllegalStateException("Impossible: Should never use BEST renderer");
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderToBuffer(DirectVertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
|
||||
var sink = ShaderMod.get().getQuadEmitter(size, MonitorBlockEntityRenderer::getBuffer);
|
||||
var buffer = sink.buffer();
|
||||
|
||||
draw.accept(sink);
|
||||
buffer.flip();
|
||||
vbo.upload(buffer.limit() / sink.format().getVertexSize(), RenderTypes.TERMINAL.mode(), sink.format(), buffer);
|
||||
}
|
||||
|
||||
private static void tboVertex(VertexConsumer builder, Matrix4f matrix, float x, float y) {
|
||||
// We encode position in the UV, as that's not transformed by the matrix.
|
||||
builder.addVertex(matrix, x, y, 0).setUv(x, y);
|
||||
}
|
||||
|
||||
private static ByteBuffer getBuffer(int capacity) {
|
||||
var buffer = backingBuffer;
|
||||
if (buffer == null || buffer.capacity() < capacity) {
|
||||
buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
|
||||
renderState.vertexCountAfterBackground = vertexCountAfterBackground;
|
||||
renderState.vertexCountAfterForeground = vertexCountAfterForeground;
|
||||
renderState.vertexCountAfterCursor = vertexCountAfterCursor;
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
return buffer;
|
||||
if (renderState.vertexCountAfterCursor == 0) return;
|
||||
|
||||
// Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will
|
||||
// use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the
|
||||
// normal render distance (~200), and the edges of the monitor fade out due to fog.
|
||||
// There's not really a good way around this, at least without using a custom render type (which the VBO
|
||||
// renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
|
||||
// absurdly high value.
|
||||
var oldFog = RenderSystem.getShaderFog();
|
||||
RenderSystem.setShaderFog(Minecraft.getInstance().gameRenderer.fogRenderer.getBuffer(FogRenderer.FogMode.NONE));
|
||||
|
||||
// Compose the existing model view matrix with our transformation matrix.
|
||||
RenderSystem.getModelViewStack().pushMatrix();
|
||||
RenderSystem.getModelViewStack().mul(matrix);
|
||||
|
||||
// Render background geometry
|
||||
drawWithShader(renderState, FixedWidthFontRenderer.TERMINAL_TEXT, RenderPipelines.TEXT, 0, renderState.vertexCountAfterBackground);
|
||||
drawWithShader(
|
||||
renderState, FixedWidthFontRenderer.TERMINAL_TEXT_OFFSET, RenderPipelines.TEXT_POLYGON_OFFSET, renderState.vertexCountAfterBackground,
|
||||
(
|
||||
FixedWidthFontRenderer.isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()
|
||||
? renderState.vertexCountAfterCursor : renderState.vertexCountAfterForeground
|
||||
) - renderState.vertexCountAfterBackground
|
||||
);
|
||||
|
||||
// Clear state
|
||||
RenderSystem.getModelViewStack().popMatrix();
|
||||
RenderSystem.setShaderFog(oldFog);
|
||||
}
|
||||
|
||||
private static void drawWithShader(MonitorRenderState renderState, RenderType renderType, RenderPipeline pipeline, int vertexOffset, int vertexCount) {
|
||||
if (renderState.vertexBuffer == null) {
|
||||
throw new IllegalStateException("MonitorRenderState has not been initialised");
|
||||
}
|
||||
if (vertexCount == 0) return;
|
||||
|
||||
var transforms = RenderSystem.getDynamicUniforms().writeTransform(
|
||||
RenderSystem.getModelViewMatrix(),
|
||||
new Vector4f(1.0F, 1.0F, 1.0F, 1.0F),
|
||||
RenderSystem.getModelOffset(),
|
||||
RenderSystem.getTextureMatrix(),
|
||||
RenderSystem.getShaderLineWidth()
|
||||
);
|
||||
|
||||
renderType.setupRenderState();
|
||||
|
||||
var autoStorageBuffer = RenderSystem.getSequentialBuffer(renderType.mode());
|
||||
var indexCount = FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(vertexCount);
|
||||
var indexBuffer = autoStorageBuffer.getBuffer(indexCount);
|
||||
|
||||
var target = Minecraft.getInstance().getMainRenderTarget();
|
||||
var colourTarget = RenderSystem.outputColorTextureOverride != null ? RenderSystem.outputColorTextureOverride : target.getColorTextureView();
|
||||
var depthTarget = target.useDepth
|
||||
? (RenderSystem.outputDepthTextureOverride != null ? RenderSystem.outputDepthTextureOverride : target.getDepthTextureView())
|
||||
: null;
|
||||
|
||||
try (var renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Monitor", colourTarget, OptionalInt.empty(), depthTarget, OptionalDouble.empty()
|
||||
)) {
|
||||
renderPass.setPipeline(pipeline);
|
||||
|
||||
RenderSystem.bindDefaultUniforms(renderPass);
|
||||
renderPass.setUniform("DynamicTransforms", transforms);
|
||||
renderPass.setVertexBuffer(0, renderState.vertexBuffer);
|
||||
renderPass.setIndexBuffer(indexBuffer, autoStorageBuffer.type());
|
||||
|
||||
for (var j = 0; j < 12; j++) {
|
||||
var gpuTexture = RenderSystem.getShaderTexture(j);
|
||||
if (gpuTexture != null) renderPass.bindSampler("Sampler" + j, gpuTexture);
|
||||
}
|
||||
|
||||
renderPass.drawIndexed(vertexOffset, 0, indexCount, 1);
|
||||
}
|
||||
|
||||
renderType.clearRenderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -268,27 +272,13 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
|
||||
return monitor.getRenderBoundingBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if any monitors were rendered this frame.
|
||||
*
|
||||
* @return Whether any monitors were rendered.
|
||||
*/
|
||||
public static boolean hasRenderedThisFrame() {
|
||||
return FrameInfo.getRenderFrame() == lastFrame;
|
||||
}
|
||||
private static ByteBuffer getBuffer(int capacity) {
|
||||
var buffer = backingBuffer;
|
||||
if (buffer == null || buffer.capacity() < capacity) {
|
||||
buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current renderer to use.
|
||||
*
|
||||
* @return The current renderer. Will not return {@link MonitorRenderer#BEST}.
|
||||
*/
|
||||
public static MonitorRenderer currentRenderer() {
|
||||
var current = Config.monitorRenderer;
|
||||
if (current == MonitorRenderer.BEST) current = Config.monitorRenderer = bestRenderer();
|
||||
return current;
|
||||
}
|
||||
|
||||
private static MonitorRenderer bestRenderer() {
|
||||
return MonitorRenderer.VBO;
|
||||
buffer.clear();
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
@@ -6,13 +6,12 @@ package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.render.BlockOutlineRenderer;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
@@ -30,11 +29,10 @@ public final class MonitorHighlightRenderer {
|
||||
// Preserve normal behaviour when crouching.
|
||||
if (camera.getEntity().isCrouching()) return false;
|
||||
|
||||
var world = camera.getEntity().getCommandSenderWorld();
|
||||
var world = camera.getEntity().level();
|
||||
var pos = hit.getBlockPos();
|
||||
|
||||
var tile = world.getBlockEntity(pos);
|
||||
if (!(tile instanceof MonitorBlockEntity monitor)) return false;
|
||||
if (!(world.getBlockEntity(pos) instanceof MonitorBlockEntity monitor)) return false;
|
||||
|
||||
// Determine which sides are part of the external faces of the monitor, and so which need to be rendered.
|
||||
var faces = EnumSet.allOf(Direction.class);
|
||||
@@ -49,39 +47,37 @@ public final class MonitorHighlightRenderer {
|
||||
transformStack.pushPose();
|
||||
transformStack.translate(pos.getX() - cameraPos.x(), pos.getY() - cameraPos.y(), pos.getZ() - cameraPos.z());
|
||||
|
||||
// I wish I could think of a better way to do this
|
||||
var buffer = bufferSource.getBuffer(RenderType.lines());
|
||||
var transform = transformStack.last().pose();
|
||||
var normal = transformStack.last();
|
||||
if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 0, UP);
|
||||
if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 1, UP);
|
||||
if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 0, UP);
|
||||
if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 1, UP);
|
||||
if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, EAST);
|
||||
if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 1, EAST);
|
||||
if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, EAST);
|
||||
if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 1, EAST);
|
||||
if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, SOUTH);
|
||||
if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, normal, 1, 0, 0, SOUTH);
|
||||
if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, SOUTH);
|
||||
if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, normal, 1, 1, 0, SOUTH);
|
||||
var transform = transformStack.last();
|
||||
BlockOutlineRenderer.render(bufferSource, (buffer, colour) -> draw(buffer, transform, faces, colour));
|
||||
|
||||
transformStack.popPose();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void line(VertexConsumer buffer, Matrix4f transform, PoseStack.Pose normal, float x, float y, float z, Direction direction) {
|
||||
private static void draw(VertexConsumer buffer, PoseStack.Pose transform, EnumSet<Direction> faces, int colour) {
|
||||
// I wish I could think of a better way to do this
|
||||
if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, 0, 0, 0, UP, colour);
|
||||
if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, 0, 0, 1, UP, colour);
|
||||
if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, 1, 0, 0, UP, colour);
|
||||
if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, 1, 0, 1, UP, colour);
|
||||
if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 0, EAST, colour);
|
||||
if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 1, EAST, colour);
|
||||
if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, 0, 1, 0, EAST, colour);
|
||||
if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, 0, 1, 1, EAST, colour);
|
||||
if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 0, SOUTH, colour);
|
||||
if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, 1, 0, 0, SOUTH, colour);
|
||||
if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, 0, 1, 0, SOUTH, colour);
|
||||
if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, 1, 1, 0, SOUTH, colour);
|
||||
}
|
||||
|
||||
private static void line(VertexConsumer buffer, PoseStack.Pose transform, float x, float y, float z, Direction direction, int colour) {
|
||||
buffer
|
||||
.addVertex(transform, x, y, z)
|
||||
.setColor(0, 0, 0, 0.4f)
|
||||
.setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
|
||||
.setColor(colour)
|
||||
.setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
|
||||
buffer
|
||||
.addVertex(transform,
|
||||
x + direction.getStepX(),
|
||||
y + direction.getStepY(),
|
||||
z + direction.getStepZ()
|
||||
)
|
||||
.setColor(0, 0, 0, 0.4f)
|
||||
.setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
|
||||
.addVertex(transform, x + direction.getStepX(), y + direction.getStepY(), z + direction.getStepZ())
|
||||
.setColor(colour)
|
||||
.setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
|
||||
}
|
||||
}
|
||||
|
@@ -5,17 +5,10 @@
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import dan200.computercraft.client.render.vbo.DirectBuffers;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
import com.mojang.blaze3d.buffers.GpuBuffer;
|
||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -27,97 +20,39 @@ import java.util.Set;
|
||||
* This is automatically cleared by {@link dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity} when the
|
||||
* entity is unloaded on the client side (see {@link MonitorRenderState#close()}).
|
||||
*/
|
||||
public class MonitorRenderState implements ClientMonitor.RenderState {
|
||||
public final class MonitorRenderState implements ClientMonitor.RenderState {
|
||||
@GuardedBy("allMonitors")
|
||||
private static final Set<MonitorRenderState> allMonitors = new HashSet<>();
|
||||
|
||||
public long lastRenderFrame = -1;
|
||||
public @Nullable BlockPos lastRenderPos = null;
|
||||
long lastRenderFrame = -1;
|
||||
@Nullable
|
||||
BlockPos lastRenderPos = null;
|
||||
|
||||
public int tboBuffer;
|
||||
public int tboTexture;
|
||||
public int tboUniform;
|
||||
public @Nullable DirectVertexBuffer backgroundBuffer;
|
||||
public @Nullable DirectVertexBuffer foregroundBuffer;
|
||||
@Nullable
|
||||
GpuBuffer vertexBuffer;
|
||||
|
||||
/**
|
||||
* Create the appropriate buffer if needed.
|
||||
*
|
||||
* @param renderer The renderer to use.
|
||||
* @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer,
|
||||
* or this mode does not require one.
|
||||
*/
|
||||
public boolean createBuffer(MonitorRenderer renderer) {
|
||||
switch (renderer) {
|
||||
case TBO: {
|
||||
if (tboBuffer != 0) return false;
|
||||
int vertexCountAfterBackground;
|
||||
int vertexCountAfterForeground;
|
||||
int vertexCountAfterCursor;
|
||||
|
||||
deleteBuffers();
|
||||
void register() {
|
||||
if (vertexBuffer != null) return;
|
||||
|
||||
tboBuffer = DirectBuffers.createBuffer();
|
||||
DirectBuffers.setEmptyBufferData(GL31.GL_TEXTURE_BUFFER, tboBuffer, GL15.GL_STATIC_DRAW);
|
||||
tboTexture = GlStateManager._genTexture();
|
||||
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, tboTexture);
|
||||
GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL30.GL_R8UI, tboBuffer);
|
||||
GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, 0);
|
||||
|
||||
tboUniform = DirectBuffers.createBuffer();
|
||||
DirectBuffers.setEmptyBufferData(GL31.GL_UNIFORM_BUFFER, tboUniform, GL15.GL_STATIC_DRAW);
|
||||
|
||||
addMonitor();
|
||||
return true;
|
||||
}
|
||||
|
||||
case VBO:
|
||||
if (backgroundBuffer != null) return false;
|
||||
|
||||
deleteBuffers();
|
||||
backgroundBuffer = new DirectVertexBuffer();
|
||||
foregroundBuffer = new DirectVertexBuffer();
|
||||
addMonitor();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addMonitor() {
|
||||
synchronized (allMonitors) {
|
||||
allMonitors.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBuffers() {
|
||||
if (tboBuffer != 0) {
|
||||
DirectBuffers.deleteBuffer(GL31.GL_TEXTURE_BUFFER, tboBuffer);
|
||||
tboBuffer = 0;
|
||||
}
|
||||
|
||||
if (tboTexture != 0) {
|
||||
GlStateManager._deleteTexture(tboTexture);
|
||||
tboTexture = 0;
|
||||
}
|
||||
|
||||
if (tboUniform != 0) {
|
||||
DirectBuffers.deleteBuffer(GL31.GL_UNIFORM_BUFFER, tboUniform);
|
||||
tboUniform = 0;
|
||||
}
|
||||
|
||||
if (backgroundBuffer != null) {
|
||||
backgroundBuffer.close();
|
||||
backgroundBuffer = null;
|
||||
}
|
||||
|
||||
if (foregroundBuffer != null) {
|
||||
foregroundBuffer.close();
|
||||
foregroundBuffer = null;
|
||||
if (vertexBuffer != null) {
|
||||
vertexBuffer.close();
|
||||
vertexBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (tboBuffer != 0 || backgroundBuffer != null) {
|
||||
if (vertexBuffer != null) {
|
||||
synchronized (allMonitors) {
|
||||
allMonitors.remove(this);
|
||||
}
|
||||
|
@@ -1,127 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render.monitor;
|
||||
|
||||
import com.mojang.blaze3d.shaders.Uniform;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.client.FrameInfo;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL31;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.getColour;
|
||||
|
||||
/**
|
||||
* The shader used for the monitor TBO renderer.
|
||||
* <p>
|
||||
* This extends Minecraft's default shader loading code to extract out the TBO buffer and handle our custom uniforms
|
||||
* ({@code MonitorData}, {@code CursorBlink}).
|
||||
* <p>
|
||||
* See also {@code monitor_tbo.fsh} and {@code monitor_tbo.vsh} in the mod's resources.
|
||||
*
|
||||
* @see RenderTypes#getMonitorTextureBufferShader()
|
||||
*/
|
||||
public class MonitorTextureBufferShader extends ShaderInstance {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MonitorTextureBufferShader.class);
|
||||
|
||||
public static final int UNIFORM_SIZE = 4 * 4 * 16 + 4 + 4 + 2 * 4 + 4;
|
||||
|
||||
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
|
||||
|
||||
private final int monitorData;
|
||||
private int uniformBuffer = 0;
|
||||
|
||||
private final @Nullable Uniform cursorBlink;
|
||||
|
||||
public MonitorTextureBufferShader(ResourceProvider provider, String location, VertexFormat format) throws IOException {
|
||||
super(provider, location, format);
|
||||
monitorData = GL31.glGetUniformBlockIndex(getId(), "MonitorData");
|
||||
if (monitorData == -1) throw new IllegalStateException("Could not find MonitorData uniform.");
|
||||
|
||||
cursorBlink = getUniformChecked("CursorBlink");
|
||||
|
||||
var tbo = getUniformChecked("Tbo");
|
||||
if (tbo != null) tbo.set(TEXTURE_INDEX - GL13.GL_TEXTURE0);
|
||||
}
|
||||
|
||||
public void setupUniform(int buffer) {
|
||||
uniformBuffer = buffer;
|
||||
|
||||
var cursorAlpha = FrameInfo.getGlobalCursorBlink() ? 1 : 0;
|
||||
if (cursorBlink != null && cursorBlink.getIntBuffer().get(0) != cursorAlpha) cursorBlink.set(cursorAlpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
super.apply();
|
||||
GL31.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, monitorData, uniformBuffer);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Uniform getUniformChecked(String name) {
|
||||
var uniform = getUniform(name);
|
||||
if (uniform == null) {
|
||||
LOG.warn("Monitor shader {} should have uniform {}, but it was not present.", getName(), name);
|
||||
}
|
||||
|
||||
return uniform;
|
||||
}
|
||||
|
||||
public static void setTerminalData(ByteBuffer buffer, Terminal terminal) {
|
||||
int width = terminal.getWidth(), height = terminal.getHeight();
|
||||
|
||||
var pos = 0;
|
||||
for (var y = 0; y < height; y++) {
|
||||
TextBuffer text = terminal.getLine(y), textColour = terminal.getTextColourLine(y), background = terminal.getBackgroundColourLine(y);
|
||||
for (var x = 0; x < width; x++) {
|
||||
buffer.put(pos, (byte) (text.charAt(x) & 0xFF));
|
||||
buffer.put(pos + 1, (byte) getColour(textColour.charAt(x), Colour.WHITE));
|
||||
buffer.put(pos + 2, (byte) getColour(background.charAt(x), Colour.BLACK));
|
||||
pos += 3;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.limit(pos);
|
||||
}
|
||||
|
||||
public static void setUniformData(ByteBuffer buffer, Terminal terminal) {
|
||||
var pos = 0;
|
||||
var palette = terminal.getPalette();
|
||||
for (var i = 0; i < 16; i++) {
|
||||
{
|
||||
var colour = palette.getColour(i);
|
||||
if (!terminal.isColour()) {
|
||||
var f = FixedWidthFontRenderer.toGreyscale(colour);
|
||||
buffer.putFloat(pos, f).putFloat(pos + 4, f).putFloat(pos + 8, f);
|
||||
} else {
|
||||
buffer.putFloat(pos, (float) colour[0]).putFloat(pos + 4, (float) colour[1]).putFloat(pos + 8, (float) colour[2]);
|
||||
}
|
||||
}
|
||||
|
||||
pos += 4 * 4; // std140 requires these are 4-wide
|
||||
}
|
||||
|
||||
var showCursor = FixedWidthFontRenderer.isCursorVisible(terminal);
|
||||
buffer
|
||||
.putInt(pos, terminal.getWidth()).putInt(pos + 4, terminal.getHeight())
|
||||
.putInt(pos + 8, showCursor ? terminal.getCursorX() : -2)
|
||||
.putInt(pos + 12, showCursor ? terminal.getCursorY() : -2)
|
||||
.putInt(pos + 16, 15 - terminal.getTextColour());
|
||||
|
||||
buffer.limit(UNIFORM_SIZE);
|
||||
}
|
||||
}
|
@@ -7,12 +7,11 @@ package dan200.computercraft.client.render.text;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.util.ARGB;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -34,7 +33,7 @@ import static org.lwjgl.system.MemoryUtil.*;
|
||||
* <p>
|
||||
* Note this is almost an exact copy of {@link FixedWidthFontRenderer}. While the code duplication is unfortunate,
|
||||
* it is measurably faster than introducing polymorphism into {@link FixedWidthFontRenderer}.
|
||||
*
|
||||
* <p>
|
||||
* <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
|
||||
* {@link FixedWidthFontRenderer}.
|
||||
*/
|
||||
@@ -157,26 +156,40 @@ public final class DirectFixedWidthFontRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public static int getVertexCount(Terminal terminal) {
|
||||
return (terminal.getHeight() + 2) * (terminal.getWidth() + 2) * 2;
|
||||
}
|
||||
|
||||
private static void quad(QuadEmitter buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
buffer.vertexCount += 4;
|
||||
buffer.quad(x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
|
||||
}
|
||||
|
||||
public interface QuadEmitter {
|
||||
VertexFormat format();
|
||||
public abstract static class QuadEmitter {
|
||||
private int vertexCount;
|
||||
|
||||
ByteBuffer buffer();
|
||||
public abstract ByteBuffer byteBuffer();
|
||||
|
||||
void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
|
||||
public abstract VertexFormat format();
|
||||
|
||||
protected abstract void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
|
||||
|
||||
public int vertexCount() {
|
||||
return vertexCount;
|
||||
}
|
||||
}
|
||||
|
||||
public record ByteBufferEmitter(ByteBuffer buffer) implements QuadEmitter {
|
||||
public static final class ByteBufferEmitter extends QuadEmitter {
|
||||
private final ByteBuffer buffer;
|
||||
|
||||
public ByteBufferEmitter(ByteBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer byteBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VertexFormat format() {
|
||||
return RenderTypes.TERMINAL.format();
|
||||
return TERMINAL_TEXT.format();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,23 +198,24 @@ public final class DirectFixedWidthFontRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
private static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
|
||||
// Emit a single quad to our buffer. This uses Unsafe (well, LWJGL's MemoryUtil) to directly blit bytes to the
|
||||
// underlying buffer. This allows us to have a single bounds check up-front, rather than one for every write.
|
||||
// This provides significant performance gains, at the cost of well, using Unsafe.
|
||||
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(rgba:BBBB)(uv1:FF)(uv2:SS),
|
||||
// Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(abgr:BBBB)(uv1:FF)(uv2:SS),
|
||||
// which matches the POSITION_COLOR_TEX_LIGHTMAP vertex format.
|
||||
|
||||
var position = buffer.position();
|
||||
var addr = MemoryUtil.memAddress(buffer);
|
||||
|
||||
// We're doing terrible unsafe hacks below, so let's be really sure that what we're doing is reasonable.
|
||||
if (position < 0 || 112 > buffer.limit() - position) throw new IndexOutOfBoundsException();
|
||||
// Require the pointer to be aligned to a 32-bit boundary.
|
||||
if ((addr & 3) != 0) throw new IllegalStateException("Memory is not aligned");
|
||||
if (TERMINAL_TEXT.format().getVertexSize() != 28) {
|
||||
throw new IllegalStateException("Incorrect vertex size");
|
||||
}
|
||||
|
||||
// Pack colour so it is equivalent to rgba:BBBB. This matches the logic in BufferBuilder.
|
||||
var colourAbgr = FastColor.ABGR32.fromArgb32(colour);
|
||||
var colourAbgr = ARGB.toABGR(colour);
|
||||
// Pack colour so it is equivalent to abgr:BBBB. This matches the logic in BufferBuilder.
|
||||
var nativeColour = IS_LITTLE_ENDIAN ? colourAbgr : Integer.reverseBytes(colourAbgr);
|
||||
|
||||
memPutFloat(addr + 0, x1);
|
||||
|
@@ -11,13 +11,13 @@ import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.util.ARGB;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
/**
|
||||
* Handles rendering fixed width text and computer terminals.
|
||||
* <p>
|
||||
@@ -35,6 +35,13 @@ import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMA
|
||||
public final class FixedWidthFontRenderer {
|
||||
public static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
|
||||
|
||||
/**
|
||||
* A render type for terminal text.
|
||||
*/
|
||||
public static final RenderType TERMINAL_TEXT = RenderType.text(FONT);
|
||||
|
||||
public static final RenderType TERMINAL_TEXT_OFFSET = RenderType.textPolygonOffset(FONT);
|
||||
|
||||
public static final int FONT_HEIGHT = 9;
|
||||
public static final int FONT_WIDTH = 6;
|
||||
static final float WIDTH = 256.0f;
|
||||
@@ -42,7 +49,7 @@ public final class FixedWidthFontRenderer {
|
||||
static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH;
|
||||
static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
|
||||
|
||||
private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
|
||||
private static final int BLACK = ARGB.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
|
||||
private static final float Z_OFFSET = 1e-4f;
|
||||
|
||||
private FixedWidthFontRenderer() {
|
||||
@@ -137,7 +144,7 @@ public final class FixedWidthFontRenderer {
|
||||
var rowY = y + FONT_HEIGHT * i;
|
||||
drawString(
|
||||
emitter, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
|
||||
palette, FULL_BRIGHT_LIGHTMAP
|
||||
palette, LightTexture.FULL_BRIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -152,12 +159,12 @@ public final class FixedWidthFontRenderer {
|
||||
// Top and bottom margins
|
||||
drawBackground(
|
||||
emitter, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
|
||||
leftMarginSize, rightMarginSize, topMarginSize, FULL_BRIGHT_LIGHTMAP
|
||||
leftMarginSize, rightMarginSize, topMarginSize, LightTexture.FULL_BRIGHT
|
||||
);
|
||||
|
||||
drawBackground(
|
||||
emitter, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
|
||||
leftMarginSize, rightMarginSize, bottomMarginSize, FULL_BRIGHT_LIGHTMAP
|
||||
leftMarginSize, rightMarginSize, bottomMarginSize, LightTexture.FULL_BRIGHT
|
||||
);
|
||||
|
||||
// The main text
|
||||
@@ -165,7 +172,7 @@ public final class FixedWidthFontRenderer {
|
||||
var rowY = y + FONT_HEIGHT * i;
|
||||
drawBackground(
|
||||
emitter, x, rowY, terminal.getBackgroundColourLine(i), palette,
|
||||
leftMarginSize, rightMarginSize, FONT_HEIGHT, FULL_BRIGHT_LIGHTMAP
|
||||
leftMarginSize, rightMarginSize, FONT_HEIGHT, LightTexture.FULL_BRIGHT
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -181,7 +188,7 @@ public final class FixedWidthFontRenderer {
|
||||
public static void drawCursor(QuadEmitter emitter, float x, float y, Terminal terminal) {
|
||||
if (isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()) {
|
||||
var colour = terminal.getPalette().getRenderColours(15 - terminal.getTextColour());
|
||||
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, FULL_BRIGHT_LIGHTMAP);
|
||||
drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, LightTexture.FULL_BRIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +214,7 @@ public final class FixedWidthFontRenderer {
|
||||
}
|
||||
|
||||
public static void drawEmptyTerminal(QuadEmitter emitter, float x, float y, float width, float height) {
|
||||
drawQuad(emitter, x, y, 0, width, height, BLACK, FULL_BRIGHT_LIGHTMAP);
|
||||
drawQuad(emitter, x, y, 0, width, height, BLACK, LightTexture.FULL_BRIGHT);
|
||||
}
|
||||
|
||||
public record QuadEmitter(Matrix4f poseMatrix, VertexConsumer consumer) {
|
||||
@@ -220,11 +227,10 @@ public final class FixedWidthFontRenderer {
|
||||
private static void quad(QuadEmitter c, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2, int light) {
|
||||
var poseMatrix = c.poseMatrix();
|
||||
var consumer = c.consumer();
|
||||
int r = FastColor.ARGB32.red(colour), g = FastColor.ARGB32.green(colour), b = FastColor.ARGB32.blue(colour), a = FastColor.ARGB32.alpha(colour);
|
||||
|
||||
consumer.addVertex(poseMatrix, x1, y1, z).setColor(r, g, b, a).setUv(u1, v1).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x1, y2, z).setColor(r, g, b, a).setUv(u1, v2).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x2, y2, z).setColor(r, g, b, a).setUv(u2, v2).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x2, y1, z).setColor(r, g, b, a).setUv(u2, v1).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x1, y2, z).setColor(colour).setUv(u1, v2).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x2, y2, z).setColor(colour).setUv(u2, v2).setLight(light);
|
||||
consumer.addVertex(poseMatrix, x2, y1, z).setColor(colour).setUv(u2, v1).setLight(light);
|
||||
}
|
||||
}
|
||||
|
@@ -1,71 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render.vbo;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
import net.minecraft.Util;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL15C;
|
||||
import org.lwjgl.opengl.GL45C;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Provides utilities to interact with OpenGL's buffer objects, either using direct state access or binding/unbinding
|
||||
* it.
|
||||
*/
|
||||
public class DirectBuffers {
|
||||
public static final boolean HAS_DSA;
|
||||
static final boolean ON_LINUX = Util.getPlatform() == Util.OS.LINUX;
|
||||
|
||||
static {
|
||||
var capabilities = GL.getCapabilities();
|
||||
HAS_DSA = capabilities.OpenGL45 || capabilities.GL_ARB_direct_state_access;
|
||||
}
|
||||
|
||||
public static int createBuffer() {
|
||||
return HAS_DSA ? GL45C.glCreateBuffers() : GL15C.glGenBuffers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a previously created buffer.
|
||||
* <p>
|
||||
* On Linux, {@link GlStateManager#_glDeleteBuffers(int)} clears a buffer before deleting it. However, this involves
|
||||
* binding and unbinding the buffer, conflicting with {@link BufferUploader}'s cache. This deletion method uses
|
||||
* our existing {@link #setEmptyBufferData(int, int, int)}, which correctly handles clearing the buffer.
|
||||
*
|
||||
* @param type The buffer's type.
|
||||
* @param id The buffer's ID.
|
||||
*/
|
||||
public static void deleteBuffer(int type, int id) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
if (ON_LINUX) DirectBuffers.setEmptyBufferData(type, id, GL15C.GL_DYNAMIC_DRAW);
|
||||
GL15C.glDeleteBuffers(id);
|
||||
}
|
||||
|
||||
public static void setBufferData(int type, int id, ByteBuffer buffer, int flags) {
|
||||
if (HAS_DSA) {
|
||||
GL45C.glNamedBufferData(id, buffer, flags);
|
||||
} else {
|
||||
if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
|
||||
GlStateManager._glBindBuffer(type, id);
|
||||
GlStateManager._glBufferData(type, buffer, flags);
|
||||
GlStateManager._glBindBuffer(type, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setEmptyBufferData(int type, int id, int flags) {
|
||||
if (HAS_DSA) {
|
||||
GL45C.glNamedBufferData(id, 0, flags);
|
||||
} else {
|
||||
if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
|
||||
GlStateManager._glBindBuffer(type, id);
|
||||
GlStateManager._glBufferData(type, 0, flags);
|
||||
GlStateManager._glBindBuffer(type, 0);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,77 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render.vbo;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
import com.mojang.blaze3d.vertex.VertexBuffer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import net.minecraft.client.renderer.ShaderInstance;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL15C;
|
||||
import org.lwjgl.opengl.GL45C;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A version of {@link VertexBuffer} which allows uploading {@link ByteBuffer}s directly.
|
||||
* <p>
|
||||
* This should probably be its own class (rather than subclassing), but I need access to {@link VertexBuffer#drawWithShader}.
|
||||
*/
|
||||
public class DirectVertexBuffer extends VertexBuffer {
|
||||
private int actualIndexCount;
|
||||
|
||||
public DirectVertexBuffer() {
|
||||
super(Usage.STATIC);
|
||||
if (DirectBuffers.HAS_DSA) {
|
||||
RenderSystem.glDeleteBuffers(vertexBufferId);
|
||||
if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
|
||||
vertexBufferId = GL45C.glCreateBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
public void upload(int vertexCount, VertexFormat.Mode mode, VertexFormat format, ByteBuffer buffer) {
|
||||
bind();
|
||||
|
||||
this.mode = mode;
|
||||
actualIndexCount = indexCount = mode.indexCount(vertexCount);
|
||||
indexType = VertexFormat.IndexType.SHORT;
|
||||
|
||||
RenderSystem.assertOnRenderThread();
|
||||
|
||||
DirectBuffers.setBufferData(GL15.GL_ARRAY_BUFFER, vertexBufferId, buffer, GL15.GL_STATIC_DRAW);
|
||||
if (format != this.format) {
|
||||
if (this.format != null) this.format.clearBufferState();
|
||||
this.format = format;
|
||||
|
||||
GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, vertexBufferId);
|
||||
format.setupBufferState();
|
||||
GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
var indexBuffer = RenderSystem.getSequentialBuffer(mode);
|
||||
if (indexBuffer != sequentialIndices || !indexBuffer.hasStorage(indexCount)) {
|
||||
indexBuffer.bind(indexCount);
|
||||
sequentialIndices = indexBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
public void drawWithShader(Matrix4f modelView, Matrix4f projection, ShaderInstance shader, int indexCount) {
|
||||
this.indexCount = indexCount;
|
||||
drawWithShader(modelView, projection, shader);
|
||||
this.indexCount = actualIndexCount;
|
||||
}
|
||||
|
||||
public int getIndexCount() {
|
||||
return actualIndexCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
|
||||
}
|
||||
}
|
@@ -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,61 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.client.turtle;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.client.ModelLocation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
|
||||
import dan200.computercraft.shared.util.DataComponentUtil;
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link TurtleUpgradeModeller} for modems, providing different models depending on if the modem is on/off.
|
||||
*/
|
||||
public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
|
||||
@Override
|
||||
public TransformedModel getModel(TurtleModem upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
|
||||
var active = DataComponentUtil.isPresent(data, ModRegistry.DataComponents.ON.get(), x -> x);
|
||||
|
||||
var models = upgrade.advanced() ? ModemModels.ADVANCED : ModemModels.NORMAL;
|
||||
return side == TurtleSide.LEFT
|
||||
? TransformedModel.of(active ? models.leftOnModel() : models.leftOffModel())
|
||||
: TransformedModel.of(active ? models.rightOnModel() : models.rightOffModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of(ModemModels.NORMAL, ModemModels.ADVANCED).flatMap(ModemModels::getDependencies);
|
||||
}
|
||||
|
||||
private record ModemModels(
|
||||
ModelLocation leftOffModel, ModelLocation rightOffModel,
|
||||
ModelLocation leftOnModel, ModelLocation rightOnModel
|
||||
) {
|
||||
private static final ModemModels NORMAL = create("normal");
|
||||
private static final ModemModels ADVANCED = create("advanced");
|
||||
|
||||
public static ModemModels create(String type) {
|
||||
return new ModemModels(
|
||||
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left")),
|
||||
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right")),
|
||||
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left")),
|
||||
ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right"))
|
||||
);
|
||||
}
|
||||
|
||||
public Stream<ResourceLocation> getDependencies() {
|
||||
return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel).flatMap(ModelLocation::getDependencies);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user