mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-03 23:22:59 +00:00 
			
		
		
		
	Compare commits
	
		
			79 Commits
		
	
	
		
			v1.21.1-1.
			...
			v1.21.1-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					fbf994e803 | ||
| 
						 | 
					8344c0a5c2 | ||
| 
						 | 
					a292d33830 | ||
| 
						 | 
					341d1c7bc2 | ||
| 
						 | 
					531eacfac7 | ||
| 
						 | 
					1f3da5205c | ||
| 
						 | 
					798ceefafe | ||
| 
						 | 
					7c0f79fc3c | ||
| 
						 | 
					b35cefc5dd | ||
| 
						 | 
					89dd521930 | ||
| 
						 | 
					9272e2efcd | ||
| 
						 | 
					69353a4fcf | ||
| 
						 | 
					ff363dca5a | ||
| 
						 | 
					1c51282426 | ||
| 
						 | 
					4a3a1c9275 | ||
| 
						 | 
					2557dd0af9 | ||
| 
						 | 
					b5c0c6e104 | ||
| 
						 | 
					876fd8ddb8 | ||
| 
						 | 
					ee3b1343b5 | ||
| 
						 | 
					b440b964b7 | ||
| 
						 | 
					5dfc401b45 | ||
| 
						 | 
					418c9be7ac | ||
| 
						 | 
					b491f6b11f | ||
| 
						 | 
					4344c3072f | ||
| 
						 | 
					947001104d | ||
| 
						 | 
					8711512769 | ||
| 
						 | 
					fdae94b3c1 | ||
| 
						 | 
					9c0ce27ce6 | ||
| 
						 | 
					c458360b18 | ||
| 
						 | 
					09ad6c1905 | ||
| 
						 | 
					0e1e8a72d3 | ||
| 
						 | 
					ffa6eadc26 | ||
| 
						 | 
					7c1e8e1951 | ||
| 
						 | 
					b805a34c2d | ||
| 
						 | 
					b03546a158 | ||
| 
						 | 
					582713467f | ||
| 
						 | 
					b6f41a0df5 | ||
| 
						 | 
					594738a022 | ||
| 
						 | 
					27f2ab364c | ||
| 
						 | 
					5a43273757 | ||
| 
						 | 
					97e28516fb | ||
| 
						 | 
					676fb5fb53 | ||
| 
						 | 
					08dc08b5a3 | ||
| 
						 | 
					8f4d4038f6 | ||
| 
						 | 
					63ba3fe274 | ||
| 
						 | 
					749b3df227 | ||
| 
						 | 
					b97634b717 | ||
| 
						 | 
					1b8344d0a3 | ||
| 
						 | 
					b42bc0a01a | ||
| 
						 | 
					70a7478529 | ||
| 
						 | 
					0cff73e2fc | ||
| 
						 | 
					a892739f8e | ||
| 
						 | 
					f8785a092f | ||
| 
						 | 
					598fc4aefd | ||
| 
						 | 
					dd7e8fcefc | ||
| 
						 | 
					29c8f96912 | ||
| 
						 | 
					b9267ecbfc | ||
| 
						 | 
					9d2c2db22b | ||
| 
						 | 
					6660966320 | ||
| 
						 | 
					3acb231f01 | ||
| 
						 | 
					16324e1eac | ||
| 
						 | 
					fa33949113 | ||
| 
						 | 
					0c04d9de47 | ||
| 
						 | 
					32f5c38485 | ||
| 
						 | 
					01fe949b3e | ||
| 
						 | 
					c03fce275e | ||
| 
						 | 
					0998acaa82 | ||
| 
						 | 
					12a44fed6f | ||
| 
						 | 
					3f8c3b026a | ||
| 
						 | 
					0a8d505323 | ||
| 
						 | 
					237a0ac3bb | ||
| 
						 | 
					b185d088b3 | ||
| 
						 | 
					051c70a731 | ||
| 
						 | 
					2e2f308ff3 | ||
| 
						 | 
					0f123b5efd | ||
| 
						 | 
					1278246cf7 | ||
| 
						 | 
					88cb03be6b | ||
| 
						 | 
					1e25fa9bc3 | ||
| 
						 | 
					74f707aaea | 
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
# See https://pre-commit.com/hooks.html for more hooks
 | 
			
		||||
repos:
 | 
			
		||||
- repo: https://github.com/pre-commit/pre-commit-hooks
 | 
			
		||||
  rev: v4.4.0
 | 
			
		||||
  rev: v5.0.0
 | 
			
		||||
  hooks:
 | 
			
		||||
  - id: trailing-whitespace
 | 
			
		||||
  - id: end-of-file-fixer
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
@@ -48,7 +48,7 @@ repositories {
 | 
			
		||||
            includeGroup("cc.tweaked")
 | 
			
		||||
            // Things we mirror
 | 
			
		||||
            includeGroup("com.simibubi.create")
 | 
			
		||||
            includeGroup("commoble.morered")
 | 
			
		||||
            includeGroup("net.commoble.morered")
 | 
			
		||||
            includeGroup("dev.architectury")
 | 
			
		||||
            includeGroup("dev.emi")
 | 
			
		||||
            includeGroup("maven.modrinth")
 | 
			
		||||
@@ -91,9 +91,9 @@ sourceSets.all {
 | 
			
		||||
 | 
			
		||||
        options.errorprone {
 | 
			
		||||
            check("InvalidBlockTag", CheckSeverity.OFF) // Broken by @cc.xyz
 | 
			
		||||
            check("InvalidParam", CheckSeverity.OFF) // Broken by records.
 | 
			
		||||
            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.
 | 
			
		||||
@@ -122,7 +122,6 @@ tasks.compileTestJava {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
tasks.withType(JavaCompile::class.java).configureEach {
 | 
			
		||||
    options.encoding = "UTF-8"
 | 
			
		||||
}
 | 
			
		||||
@@ -171,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) {
 | 
			
		||||
@@ -195,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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -101,7 +101,10 @@ SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
        <module name="InvalidJavadocPosition" />
 | 
			
		||||
        <module name="JavadocBlockTagLocation" />
 | 
			
		||||
        <module name="JavadocMethod"/>
 | 
			
		||||
        <module name="JavadocType"/>
 | 
			
		||||
        <module name="JavadocType">
 | 
			
		||||
            <!-- Seems to complain about @hidden!? -->
 | 
			
		||||
            <property name="allowUnknownTags" value="true" />
 | 
			
		||||
        </module>
 | 
			
		||||
        <module name="JavadocStyle">
 | 
			
		||||
            <property name="checkHtml" value="false" />
 | 
			
		||||
        </module>
 | 
			
		||||
@@ -152,7 +155,10 @@ SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
        <module name="NoWhitespaceAfter">
 | 
			
		||||
            <property name="tokens" value="AT,INC,DEC,UNARY_MINUS,UNARY_PLUS,BNOT,LNOT,DOT,ARRAY_DECLARATOR,INDEX_OP,METHOD_REF" />
 | 
			
		||||
        </module>
 | 
			
		||||
        <module name="NoWhitespaceBefore" />
 | 
			
		||||
        <module name="NoWhitespaceBefore">
 | 
			
		||||
            <!-- Allow whitespace before "..." for @Nullable annotations -->
 | 
			
		||||
            <property name="tokens" value="COMMA,SEMI,POST_INC,POST_DEC,LABELED_STAT" />
 | 
			
		||||
        </module>
 | 
			
		||||
        <!-- TODO: Decide on an OperatorWrap style. -->
 | 
			
		||||
        <module name="ParenPad" />
 | 
			
		||||
        <module name="SeparatorWrap">
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
isUnstable=true
 | 
			
		||||
modVersion=1.114.4
 | 
			
		||||
modVersion=1.116.0
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.21.1
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
fabric-api = "0.102.1+1.21.1"
 | 
			
		||||
fabric-loader = "0.15.11"
 | 
			
		||||
neoForge = "21.1.9"
 | 
			
		||||
neoForgeSpi = "8.0.1"
 | 
			
		||||
neoMergeTool = "2.0.0"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2024.07.28"
 | 
			
		||||
parchmentMc = "1.21"
 | 
			
		||||
@@ -26,12 +26,12 @@ slf4j = "2.0.9"
 | 
			
		||||
asm = "9.6"
 | 
			
		||||
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"
 | 
			
		||||
jsr305 = "3.0.2"
 | 
			
		||||
jspecify = "1.0.0"
 | 
			
		||||
jzlib = "1.1.3"
 | 
			
		||||
kotlin = "2.1.0"
 | 
			
		||||
kotlin = "2.1.10"
 | 
			
		||||
kotlin-coroutines = "1.10.1"
 | 
			
		||||
nightConfig = "3.8.1"
 | 
			
		||||
 | 
			
		||||
@@ -42,12 +42,12 @@ 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"
 | 
			
		||||
moreRed = "4.0.0.4"
 | 
			
		||||
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"
 | 
			
		||||
mixinExtra = "0.3.5"
 | 
			
		||||
create-forge = "0.5.1.f-33"
 | 
			
		||||
create-forge = "6.0.0-6"
 | 
			
		||||
create-fabric = "0.5.1-f-build.1467+mc1.20.1"
 | 
			
		||||
 | 
			
		||||
# Testing
 | 
			
		||||
@@ -58,24 +58,24 @@ junitPlatform = "1.11.4"
 | 
			
		||||
jmh = "1.37"
 | 
			
		||||
 | 
			
		||||
# Build tools
 | 
			
		||||
cctJavadoc = "1.8.3"
 | 
			
		||||
checkstyle = "10.20.1"
 | 
			
		||||
errorProne-core = "2.27.0"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
fabric-loom = "1.9.2"
 | 
			
		||||
cctJavadoc = "1.8.4"
 | 
			
		||||
checkstyle = "10.23.1"
 | 
			
		||||
errorProne-core = "2.38.0"
 | 
			
		||||
errorProne-plugin = "4.1.0"
 | 
			
		||||
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.10.25"
 | 
			
		||||
modDevGradle = "2.0.95"
 | 
			
		||||
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"
 | 
			
		||||
vanillaExtract = "0.2.1"
 | 
			
		||||
versionCatalogUpdate = "0.8.1"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
@@ -87,10 +87,10 @@ 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" }
 | 
			
		||||
jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" }
 | 
			
		||||
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
 | 
			
		||||
jzlib = { module = "com.jcraft:jzlib", version.ref = "jzlib" }
 | 
			
		||||
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
 | 
			
		||||
kotlin-platform = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
 | 
			
		||||
@@ -105,7 +105,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
 | 
			
		||||
 | 
			
		||||
# Minecraft mods
 | 
			
		||||
create-fabric = { module = "com.simibubi.create:create-fabric-1.20.1", version.ref = "create-fabric" }
 | 
			
		||||
create-forge = { module = "com.simibubi.create:create-1.20.1", version.ref = "create-forge" }
 | 
			
		||||
create-forge = { module = "com.simibubi.create:create-1.21.1", version.ref = "create-forge" }
 | 
			
		||||
emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" }
 | 
			
		||||
fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" }
 | 
			
		||||
fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric-loader" }
 | 
			
		||||
@@ -119,7 +119,7 @@ jei-forge = { module = "mezz.jei:jei-1.21-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" }
 | 
			
		||||
moreRed = { module = "commoble.morered:morered-1.20.1", version.ref = "moreRed" }
 | 
			
		||||
moreRed = { module = "net.commoble.morered:morered-1.21.1", version.ref = "moreRed" }
 | 
			
		||||
rei-api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" }
 | 
			
		||||
rei-builtin = { module = "me.shedaniel:RoughlyEnoughItems-default-plugin", version.ref = "rei" }
 | 
			
		||||
rei-fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" }
 | 
			
		||||
@@ -180,7 +180,7 @@ taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
 | 
			
		||||
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }
 | 
			
		||||
 | 
			
		||||
[bundles]
 | 
			
		||||
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
 | 
			
		||||
annotations = ["checkerFramework", "jetbrainsAnnotations", "jspecify"]
 | 
			
		||||
kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
 | 
			
		||||
 | 
			
		||||
# Minecraft
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										969
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										969
									
								
								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-typescript": "^12.0.0",
 | 
			
		||||
    "@rollup/plugin-node-resolve": "^16.0.0",
 | 
			
		||||
    "@rollup/plugin-typescript": "^12.0.0 && <12.1.3",
 | 
			
		||||
    "@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",
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ 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.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,7 @@ import dan200.computercraft.impl.client.ClientPlatformHelper;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.core.component.DataComponentPatch;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
final class TurtleUpgradeModellers {
 | 
			
		||||
    private static final Transformation leftTransform = getMatrixFor(-0.4065f);
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,7 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
@ApiStatus.Internal
 | 
			
		||||
public interface ClientPlatformHelper {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,7 @@ import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The static entry point to the ComputerCraft API.
 | 
			
		||||
@@ -148,7 +147,10 @@ public final class ComputerCraftAPI {
 | 
			
		||||
     *
 | 
			
		||||
     * @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);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,8 +6,8 @@ package dan200.computercraft.api.detail;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,7 @@ import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BlockEntity;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A reference to a block in the world, used by block detail providers.
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ package dan200.computercraft.api.detail;
 | 
			
		||||
import net.minecraft.core.component.DataComponentHolder;
 | 
			
		||||
import net.minecraft.core.component.DataComponentType;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import java.util.Map;
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of object that this provider can provide details for.
 | 
			
		||||
 * @see DetailRegistry
 | 
			
		||||
 * @see dan200.computercraft.api.detail An overview of the detail system.
 | 
			
		||||
 */
 | 
			
		||||
@FunctionalInterface
 | 
			
		||||
public interface DetailProvider<T> {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import java.util.Map;
 | 
			
		||||
 * also in this package.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of object that this registry provides details for.
 | 
			
		||||
 * @see dan200.computercraft.api.detail An overview of the detail system.
 | 
			
		||||
 */
 | 
			
		||||
@ApiStatus.NonExtendable
 | 
			
		||||
public interface DetailRegistry<T> {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,9 @@ public class VanillaDetailRegistries {
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This instance's {@link DetailRegistry#getBasicDetails(Object)} is thread safe (assuming the stack is immutable)
 | 
			
		||||
     * and may be called from the computer thread.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This does not have special handling for {@linkplain ItemStack#isEmpty() empty item stacks}, and so the returned
 | 
			
		||||
     * details will be an empty stack of air. Callers should generally check for empty stacks before calling this.
 | 
			
		||||
     */
 | 
			
		||||
    public static final DetailRegistry<ItemStack> ITEM_STACK = ComputerCraftAPIService.get().getItemStackDetailRegistry();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The detail system provides a standard way for mods to return descriptions of common game objects, such as blocks or
 | 
			
		||||
 * items, as well as registering additional detail to be included in those descriptions.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * For instance, the built-in {@code turtle.getItemDetail()} method uses
 | 
			
		||||
 * {@linkplain dan200.computercraft.api.detail.VanillaDetailRegistries#ITEM_STACK in order to provide information about}
 | 
			
		||||
 * the selected item:
 | 
			
		||||
 *
 | 
			
		||||
 * <pre class="language language-lua">{@code
 | 
			
		||||
 * local item = turtle.getItemDetail(nil, true)
 | 
			
		||||
 * --[[
 | 
			
		||||
 * item = {
 | 
			
		||||
 *   name = "minecraft:wheat",
 | 
			
		||||
 *   displayName = "Wheat",
 | 
			
		||||
 *   count = 1,
 | 
			
		||||
 *   maxCount = 64,
 | 
			
		||||
 *   tags = {},
 | 
			
		||||
 * }
 | 
			
		||||
 * ]]
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Built-in detail providers</h2>
 | 
			
		||||
 * While you can define your own detail providers (perhaps for types from your own mod), CC comes with several built-in
 | 
			
		||||
 * detail registries for vanilla and mod-loader objects:
 | 
			
		||||
 *
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *     <li>{@link dan200.computercraft.api.detail.VanillaDetailRegistries}, for vanilla objects</li>
 | 
			
		||||
 *     <li>{@code dan200.computercraft.api.detail.ForgeDetailRegistries} for Forge-specific objects</li>
 | 
			
		||||
 *     <li>{@code dan200.computercraft.api.detail.FabricDetailRegistries} for Fabric-specific objects</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Example: Returning details from methods</h2>
 | 
			
		||||
 * Here we define a {@code getHeldItem()} method for pocket computers which finds the currently held item of the player
 | 
			
		||||
 * and returns it to the user using {@link dan200.computercraft.api.detail.VanillaDetailRegistries#ITEM_STACK} and
 | 
			
		||||
 * {@link dan200.computercraft.api.detail.DetailRegistry#getDetails(java.lang.Object)}.
 | 
			
		||||
 *
 | 
			
		||||
 * {@snippet class=com.example.examplemod.ExamplePocketPeripheral region=details}
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Example: Registering custom detail registries</h2>
 | 
			
		||||
 * Here we define a new detail provider for items that includes the nutrition and saturation values in the returned object.
 | 
			
		||||
 *
 | 
			
		||||
 * {@snippet class=com.example.examplemod.ExampleMod region=details}
 | 
			
		||||
 */
 | 
			
		||||
package dan200.computercraft.api.detail;
 | 
			
		||||
@@ -9,8 +9,7 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An interface passed to {@link ILuaAPIFactory} in order to provide additional information
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Construct an {@link ILuaAPI} for a computer.
 | 
			
		||||
 
 | 
			
		||||
@@ -14,14 +14,14 @@ import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.JukeboxSong;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an item that can be placed in a disk drive and used by a Computer.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Implement this interface on your {@link Item} class to allow it to be used in the drive. Alternatively, register
 | 
			
		||||
 * a {@link MediaProvider}.
 | 
			
		||||
 * Implement this interface on your {@link Item} class to allow it to be used in the drive, or register via
 | 
			
		||||
 * {@code dan200.computercraft.api.media.MediaLookup} (Fabric) or {@code dan200.computercraft.api.media.MediaCapability}
 | 
			
		||||
 * (NeoForge).
 | 
			
		||||
 */
 | 
			
		||||
public interface IMedia {
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@
 | 
			
		||||
package dan200.computercraft.api.media;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This interface is used to provide {@link IMedia} implementations for {@link ItemStack}.
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.media;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.impl.ComputerCraftAPIService;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The contents of a page (or book) created by a ComputerCraft printer.
 | 
			
		||||
 *
 | 
			
		||||
 * @since 1.115
 | 
			
		||||
 */
 | 
			
		||||
@Nullable
 | 
			
		||||
public interface PrintoutContents {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the (possibly empty) title for this printout.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The title of this printout.
 | 
			
		||||
     */
 | 
			
		||||
    String getTitle();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the text contents of this printout, as a sequence of lines.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * The lines in the printout may include blank lines at the end of the document, as well as trailing spaces on each
 | 
			
		||||
     * line.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The text contents of this printout.
 | 
			
		||||
     */
 | 
			
		||||
    Stream<String> getTextLines();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the printout contents for a particular stack.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The stack to get the contents for.
 | 
			
		||||
     * @return The printout contents, or {@code null} if this is not a printout item.
 | 
			
		||||
     */
 | 
			
		||||
    static @Nullable PrintoutContents get(ItemStack stack) {
 | 
			
		||||
        return ComputerCraftAPIService.get().getPrintoutContents(stack);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,8 +12,7 @@ import net.minecraft.world.entity.Entity;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrapper class for pocket computers.
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,7 @@ import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A peripheral which can be equipped to the back side of a pocket computer.
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,7 @@ import net.minecraft.world.Container;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The interface passed to turtle by turtles, providing methods that they can call.
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,8 @@ import net.minecraft.core.component.DataComponentPatch;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.Items;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,7 @@
 | 
			
		||||
package dan200.computercraft.api.turtle;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used to indicate the result of executing a turtle command.
 | 
			
		||||
@@ -60,9 +59,9 @@ public final class TurtleCommandResult {
 | 
			
		||||
 | 
			
		||||
    private final boolean success;
 | 
			
		||||
    private final @Nullable String errorMessage;
 | 
			
		||||
    private final @Nullable Object[] results;
 | 
			
		||||
    private final @Nullable Object @Nullable [] results;
 | 
			
		||||
 | 
			
		||||
    private TurtleCommandResult(boolean success, @Nullable String errorMessage, @Nullable Object[] results) {
 | 
			
		||||
    private TurtleCommandResult(boolean success, @Nullable String errorMessage, @Nullable Object @Nullable [] results) {
 | 
			
		||||
        this.success = success;
 | 
			
		||||
        this.errorMessage = errorMessage;
 | 
			
		||||
        this.results = results;
 | 
			
		||||
@@ -92,8 +91,7 @@ public final class TurtleCommandResult {
 | 
			
		||||
     *
 | 
			
		||||
     * @return The command's result, or {@code null} if it was a failure.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public Object[] getResults() {
 | 
			
		||||
    public @Nullable Object @Nullable [] getResults() {
 | 
			
		||||
        return results;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@ import net.minecraft.world.entity.ai.attributes.Attributes;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ 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;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
@@ -30,8 +31,7 @@ import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Backing interface for {@link ComputerCraftAPI}
 | 
			
		||||
@@ -84,6 +84,9 @@ public interface ComputerCraftAPIService {
 | 
			
		||||
 | 
			
		||||
    DetailRegistry<BlockReference> getBlockInWorldDetailRegistry();
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    PrintoutContents getPrintoutContents(ItemStack stack);
 | 
			
		||||
 | 
			
		||||
    final class Instance {
 | 
			
		||||
        static final @Nullable ComputerCraftAPIService INSTANCE;
 | 
			
		||||
        static final @Nullable Throwable ERROR;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@
 | 
			
		||||
package dan200.computercraft.impl;
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@
 | 
			
		||||
package dan200.computercraft.impl;
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.ServiceLoader;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -64,5 +64,65 @@ dependencies {
 | 
			
		||||
    CC:T), please <a href="https://github.com/cc-tweaked/CC-Tweaked/discussions/new/choose">start a discussion</a> to
 | 
			
		||||
    let me know!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<h1>Updating from Minecraft 1.20.1 to 1.21.1</h1>
 | 
			
		||||
 | 
			
		||||
<h2>Peripherals</h2>
 | 
			
		||||
<ul>
 | 
			
		||||
    <li>
 | 
			
		||||
        <p>
 | 
			
		||||
            On NeoForge, the peripheral capability has migrated to NeoForge's new capability system.
 | 
			
		||||
            <code>dan200.computercraft.api.peripheral.PeripheralCapability</code> can be used to register a peripheral.
 | 
			
		||||
            <code>IPeripheralProvider</code> has also been removed, as capabilities can now be used for arbitrary
 | 
			
		||||
            blocks.
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
    {@linkplain dan200.computercraft.api.peripheral Read more on registering peripherals}.
 | 
			
		||||
 | 
			
		||||
<h2>Turtle and pocket upgrades</h2>
 | 
			
		||||
Turtle and pocket upgrades have been migrated to use Minecraft's dynamic registries. While upgrades themselves have not
 | 
			
		||||
changed much, the interface for registering them is dramatically different.
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
    <li>
 | 
			
		||||
        <p>
 | 
			
		||||
            <code>TurtleUpgradeSerialiser</code> and <code>PocketUpgradeSerialiser</code> have been unified into a
 | 
			
		||||
            single {@link dan200.computercraft.api.upgrades.UpgradeType} class
 | 
			
		||||
        <ul>
 | 
			
		||||
            <li>
 | 
			
		||||
                Replace <code>TurtleUpgradeSerialiser.registryId()</code> with
 | 
			
		||||
                {@link dan200.computercraft.api.turtle.ITurtleUpgrade#typeRegistry()} and <code>PocketUpgradeSerialiser.registryId()</code>
 | 
			
		||||
                with {@link dan200.computercraft.api.pocket.IPocketUpgrade#typeRegistry()}.
 | 
			
		||||
            <li>
 | 
			
		||||
                Replace all other usages of <code>TurtleUpgradeSerialiser</code> and <code>PocketUpgradeSerialiser</code>
 | 
			
		||||
                with {@link dan200.computercraft.api.upgrades.UpgradeType}.
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
    <li>
 | 
			
		||||
        Upgrades are now (de)serialised using codecs, rather than manually reading from JSON and encoding/decoding
 | 
			
		||||
        network packets. Instead of subclassing {@link dan200.computercraft.api.upgrades.UpgradeType}, it is recommended
 | 
			
		||||
        you use {@link dan200.computercraft.api.upgrades.UpgradeType#create} to create a new type from a
 | 
			
		||||
        <code>MapCodec</code>.
 | 
			
		||||
 | 
			
		||||
    <li>
 | 
			
		||||
        Upgrades are no longer aware of their ID, and so cannot compute their adjective. The adjective must now either
 | 
			
		||||
        be hard-coded, or read as part of the codec.
 | 
			
		||||
 | 
			
		||||
    <li>
 | 
			
		||||
        The upgrade data providers have been removed, in favour of mod-loaders built-in support for dynamic registries.
 | 
			
		||||
        I'm afraid it's probably easier if you delete your existing upgrade datagen code and start from scratch. See
 | 
			
		||||
        <a href="./dan200/computercraft/api/turtle/ITurtleUpgrade.html#datagen">the <code>ITurtleUpgrade</code>
 | 
			
		||||
        documentation for an example</a>.
 | 
			
		||||
 | 
			
		||||
    <li>
 | 
			
		||||
        Upgrades now store their additional data ({@link dan200.computercraft.api.turtle.ITurtleAccess#getUpgradeData},
 | 
			
		||||
        {@link dan200.computercraft.api.pocket.IPocketAccess#getUpgradeData()}) as an immutable component map, rather
 | 
			
		||||
        than a compound tag.
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
    {@linkplain dan200.computercraft.api.turtle.ITurtleUpgrade Read more on registering turtle upgrades}.
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,6 @@ dependencies {
 | 
			
		||||
    compileOnly(libs.mixin)
 | 
			
		||||
    compileOnly(libs.mixinExtra)
 | 
			
		||||
    compileOnly(libs.bundles.externalMods.common)
 | 
			
		||||
    compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false }
 | 
			
		||||
    clientCompileOnly(variantOf(libs.emi) { classifier("api") })
 | 
			
		||||
 | 
			
		||||
    annotationProcessorEverywhere(libs.autoService)
 | 
			
		||||
 
 | 
			
		||||
@@ -38,8 +38,8 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,6 @@ 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;
 | 
			
		||||
@@ -41,7 +40,6 @@ import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemProperties;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceProvider;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
@@ -51,8 +49,10 @@ import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.component.DyedItemColor;
 | 
			
		||||
import net.minecraft.world.level.ItemLike;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
@@ -69,6 +69,8 @@ import java.util.function.Supplier;
 | 
			
		||||
 * @see ModRegistry The common registry for actual game objects.
 | 
			
		||||
 */
 | 
			
		||||
public final class ClientRegistry {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(ClientRegistry.class);
 | 
			
		||||
 | 
			
		||||
    private ClientRegistry() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -143,10 +145,6 @@ public final class ClientRegistry {
 | 
			
		||||
        void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
 | 
			
		||||
        register.accept(GuiSprites.initialise(minecraft.getTextureManager()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final ResourceLocation[] EXTRA_MODELS = {
 | 
			
		||||
        TurtleOverlay.ELF_MODEL,
 | 
			
		||||
        TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
 | 
			
		||||
@@ -160,7 +158,7 @@ public final class ClientRegistry {
 | 
			
		||||
 | 
			
		||||
    public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
 | 
			
		||||
        register.accept(
 | 
			
		||||
            (stack, layer) -> layer == 1 ? DiskItem.getColour(stack) : -1,
 | 
			
		||||
            (stack, layer) -> layer == 1 ? DyedItemColor.getOrDefault(stack, Colour.WHITE.getARGB()) : -1,
 | 
			
		||||
            ModRegistry.Items.DISK.get()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -192,7 +190,18 @@ public final class ClientRegistry {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
 | 
			
		||||
        RenderTypes.registerShaders(resources, load);
 | 
			
		||||
        RenderTypes.registerShaders(resources, (name, create, onLoaded) -> {
 | 
			
		||||
            ShaderInstance shader;
 | 
			
		||||
            try {
 | 
			
		||||
                shader = create.get();
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                LOG.error("Failed to load {}", name, e);
 | 
			
		||||
                onLoaded.accept(null);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            load.accept(shader, onLoaded);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record UnclampedPropertyFunction(
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,8 @@ import net.minecraft.client.gui.components.ChatComponent;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.util.Mth;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,11 @@ import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
import org.lwjgl.glfw.GLFW;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,7 @@ 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.network.chat.Component;
 | 
			
		||||
@@ -40,14 +38,15 @@ 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(
 | 
			
		||||
            computerTextures.border(),
 | 
			
		||||
            terminal.getX() - BORDER, terminal.getY() - BORDER, terminal.getWidth() + BORDER * 2, terminal.getHeight() + BORDER * 2
 | 
			
		||||
        );
 | 
			
		||||
        graphics.blitSprite(
 | 
			
		||||
            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,22 +7,16 @@ 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;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
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.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,9 +15,9 @@ import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
import org.lwjgl.glfw.GLFW;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ import net.minecraft.client.gui.components.MultiLineLabel;
 | 
			
		||||
import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
 | 
			
		||||
 
 | 
			
		||||
@@ -127,6 +127,7 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
 | 
			
		||||
        // Skip rendering labels.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("ArrayRecordComponent")
 | 
			
		||||
    record PrintoutInfo(int pages, boolean book, TextBuffer[] text, TextBuffer[] colour) {
 | 
			
		||||
        public static final PrintoutInfo DEFAULT = of(PrintoutData.EMPTY, false);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,7 @@ 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;
 | 
			
		||||
@@ -64,8 +63,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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(
 | 
			
		||||
            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();
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ import net.minecraft.client.gui.components.Button;
 | 
			
		||||
import net.minecraft.client.gui.components.Tooltip;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui.widgets;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.gui.KeyConverter;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
@@ -70,7 +71,7 @@ public class TerminalWidget extends AbstractWidget {
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean charTyped(char ch, int modifiers) {
 | 
			
		||||
        var terminalChar = StringUtil.unicodeToTerminal(ch);
 | 
			
		||||
        if (StringUtil.isTypableChar(terminalChar)) computer.charTyped(terminalChar);
 | 
			
		||||
        if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +84,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 +120,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ 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.items.PocketComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import mezz.jei.api.IModPlugin;
 | 
			
		||||
@@ -23,6 +22,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;
 | 
			
		||||
 | 
			
		||||
@@ -100,7 +100,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 IIngredientSubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DyedItemColor.getOrDefault(stack, -1));
 | 
			
		||||
 | 
			
		||||
    private static RegistryAccess getRegistryAccess() {
 | 
			
		||||
        return Minecraft.getInstance().level.registryAccess();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,90 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.model;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.client.pocket.PocketComputerData;
 | 
			
		||||
import dan200.computercraft.client.render.CustomLecternRenderer;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
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.LightTexture;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A model for {@linkplain PocketComputerItem pocket computers} placed on a lectern.
 | 
			
		||||
 *
 | 
			
		||||
 * @see CustomLecternRenderer
 | 
			
		||||
 */
 | 
			
		||||
public class LecternPocketModel {
 | 
			
		||||
    public static final ResourceLocation TEXTURE_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_normal");
 | 
			
		||||
    public static final ResourceLocation TEXTURE_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_advanced");
 | 
			
		||||
    public static final ResourceLocation TEXTURE_COLOUR = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_colour");
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    // The size of the terminal within the model.
 | 
			
		||||
    public static final float TERM_WIDTH = 12.0f / 32.0f;
 | 
			
		||||
    public static final float TERM_HEIGHT = 14.0f / 32.0f;
 | 
			
		||||
 | 
			
		||||
    // The size of the texture. The texture is 36x36, but is at 2x resolution.
 | 
			
		||||
    private static final int TEXTURE_WIDTH = 48 / 2;
 | 
			
		||||
    private static final int TEXTURE_HEIGHT = 48 / 2;
 | 
			
		||||
 | 
			
		||||
    private final ModelPart root;
 | 
			
		||||
 | 
			
		||||
    public LecternPocketModel() {
 | 
			
		||||
        root = buildPages();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ModelPart buildPages() {
 | 
			
		||||
        var mesh = new MeshDefinition();
 | 
			
		||||
        var parts = mesh.getRoot();
 | 
			
		||||
        parts.addOrReplaceChild(
 | 
			
		||||
            "root",
 | 
			
		||||
            CubeListBuilder.create().texOffs(0, 0).addBox(0f, -5.0f, -4.0f, 1f, 10.0f, 8.0f),
 | 
			
		||||
            PartPose.ZERO
 | 
			
		||||
        );
 | 
			
		||||
        return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render the pocket computer model.
 | 
			
		||||
     *
 | 
			
		||||
     * @param poseStack     The current pose stack.
 | 
			
		||||
     * @param bufferSource  The buffer source to draw to.
 | 
			
		||||
     * @param packedLight   The current light level.
 | 
			
		||||
     * @param packedOverlay The overlay texture (used for entity hurt animation).
 | 
			
		||||
     * @param family        The computer family.
 | 
			
		||||
     * @param frameColour   The pocket computer's {@linkplain DyedItemColor colour}.
 | 
			
		||||
     * @param lightColour   The pocket computer's {@linkplain PocketComputerData#getLightState() light colour}.
 | 
			
		||||
     */
 | 
			
		||||
    public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay, ComputerFamily family, int frameColour, int lightColour) {
 | 
			
		||||
        if (frameColour != -1) {
 | 
			
		||||
            root.render(poseStack, MATERIAL_FRAME.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay);
 | 
			
		||||
            root.render(poseStack, MATERIAL_COLOUR.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay, frameColour);
 | 
			
		||||
        } else {
 | 
			
		||||
            var buffer = (family == ComputerFamily.ADVANCED ? MATERIAL_ADVANCED : MATERIAL_NORMAL).buffer(bufferSource, RenderType::entityCutout);
 | 
			
		||||
            root.render(poseStack, buffer, packedLight, packedOverlay);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        root.render(poseStack, MATERIAL_LIGHT.buffer(bufferSource, RenderType::entityCutout), LightTexture.FULL_BRIGHT, packedOverlay, lightColour);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,8 +11,8 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,8 @@ 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 org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,7 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
 | 
			
		||||
    static ClientPlatformHelper get() {
 | 
			
		||||
@@ -25,5 +24,5 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
 | 
			
		||||
     * @param overlayLight  The current overlay light.
 | 
			
		||||
     * @param tints         Block colour tints to apply to the model.
 | 
			
		||||
     */
 | 
			
		||||
    void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints);
 | 
			
		||||
    void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, int @Nullable [] tints);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,7 @@ import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
 | 
			
		||||
import dan200.computercraft.shared.computer.terminal.TerminalState;
 | 
			
		||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Clientside data about a pocket computer.
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,16 +6,31 @@ package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import dan200.computercraft.client.model.LecternPocketModel;
 | 
			
		||||
import dan200.computercraft.client.model.LecternPrintoutModel;
 | 
			
		||||
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.world.item.component.DyedItemColor;
 | 
			
		||||
import net.minecraft.world.level.block.LecternBlock;
 | 
			
		||||
import net.minecraft.world.phys.Vec3;
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}.
 | 
			
		||||
@@ -23,10 +38,17 @@ import net.minecraft.world.level.block.LecternBlock;
 | 
			
		||||
 * This largely follows {@link LecternRenderer}, but with support for multiple types of item.
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
@@ -38,15 +60,53 @@ 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());
 | 
			
		||||
            }
 | 
			
		||||
        } else if (item.getItem() instanceof PocketComputerItem pocket) {
 | 
			
		||||
            var computer = ClientPocketComputers.get(item);
 | 
			
		||||
 | 
			
		||||
            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())
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
 | 
			
		||||
            poseStack.mulPose(Axis.YP.rotationDegrees(90f));
 | 
			
		||||
            poseStack.translate(-0.5 * LecternPocketModel.TERM_WIDTH, 0.5 * LecternPocketModel.TERM_HEIGHT + 1f / 32.0f, 1 / 16.0f);
 | 
			
		||||
            poseStack.mulPose(Axis.XP.rotationDegrees(180));
 | 
			
		||||
 | 
			
		||||
            // 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)) {
 | 
			
		||||
                renderPocketTerminal(poseStack, quadEmitter, terminal);
 | 
			
		||||
            } else {
 | 
			
		||||
                FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        poseStack.popPose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void renderPocketTerminal(PoseStack poseStack, FixedWidthFontRenderer.QuadEmitter quadEmitter, Terminal terminal) {
 | 
			
		||||
        var width = terminal.getWidth() * FONT_WIDTH;
 | 
			
		||||
        var height = terminal.getHeight() * FONT_HEIGHT;
 | 
			
		||||
 | 
			
		||||
        // Scale the terminal down to fit in the available space.
 | 
			
		||||
        var scaleX = LecternPocketModel.TERM_WIDTH / (width + MARGIN * 2);
 | 
			
		||||
        var scaleY = LecternPocketModel.TERM_HEIGHT / (height + MARGIN * 2);
 | 
			
		||||
        var scale = Math.min(scaleX, scaleY);
 | 
			
		||||
        poseStack.scale(scale, scale, -1.0f);
 | 
			
		||||
 | 
			
		||||
        // Convert the model dimensions to terminal space, then find out how large the margin should be.
 | 
			
		||||
        var marginX = ((LecternPocketModel.TERM_WIDTH / scale) - width) / 2;
 | 
			
		||||
        var marginY = ((LecternPocketModel.TERM_HEIGHT / scale) - height) / 2;
 | 
			
		||||
 | 
			
		||||
        FixedWidthFontRenderer.drawTerminal(quadEmitter, marginX, marginY, terminal, marginY, marginY, marginX, marginX);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -39,7 +39,7 @@ public final class ModelRenderer {
 | 
			
		||||
     * @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, @Nullable int[] tints) {
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,16 @@ 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.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
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 +32,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() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -83,14 +91,69 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
 | 
			
		||||
        var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
 | 
			
		||||
        var textures = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
 | 
			
		||||
        var spriteRenderer = new SpriteRenderer(transform, render, 0, light, colour);
 | 
			
		||||
        renderBorder(spriteRenderer, textures, width, height);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        var r = (colour >>> 16) & 0xFF;
 | 
			
		||||
        var g = (colour >>> 8) & 0xFF;
 | 
			
		||||
        var b = colour & 0xFF;
 | 
			
		||||
    private static void renderBorder(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int width, int height) {
 | 
			
		||||
        var sprites = Minecraft.getInstance().getGuiSprites();
 | 
			
		||||
 | 
			
		||||
        var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(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) {
 | 
			
		||||
@@ -101,4 +164,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
            FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final GuiSpriteScaling.NineSlice DEFAULT_BORDER = new GuiSpriteScaling.NineSlice(
 | 
			
		||||
        36, 36, new GuiSpriteScaling.NineSlice.Border(12, 12, 12, 12)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    private static final GuiSpriteScaling.NineSlice DEFAULT_BOTTOM = new GuiSpriteScaling.NineSlice(
 | 
			
		||||
        36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    private static GuiSpriteScaling.NineSlice getSlice(GuiSpriteScaling scaling, GuiSpriteScaling.NineSlice fallback) {
 | 
			
		||||
        return scaling instanceof GuiSpriteScaling.NineSlice slice ? slice : fallback;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ 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;
 | 
			
		||||
@@ -53,7 +52,7 @@ public final class PrintoutItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
        var pageData = stack.getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
 | 
			
		||||
 | 
			
		||||
        var pages = pageData.pages();
 | 
			
		||||
        var book = ((PrintoutItem) stack.getItem()).getType() == PrintoutItem.Type.BOOK;
 | 
			
		||||
        var book = stack.is(ModRegistry.Items.PRINTED_BOOK.get());
 | 
			
		||||
 | 
			
		||||
        double width = LINE_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2;
 | 
			
		||||
        double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ 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;
 | 
			
		||||
@@ -16,11 +15,11 @@ import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.client.renderer.ShaderInstance;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceProvider;
 | 
			
		||||
import org.apache.commons.io.function.IOSupplier;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -54,11 +53,6 @@ public class RenderTypes {
 | 
			
		||||
     */
 | 
			
		||||
    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;
 | 
			
		||||
@@ -68,9 +62,12 @@ public class RenderTypes {
 | 
			
		||||
        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(
 | 
			
		||||
    public interface ShaderLoader {
 | 
			
		||||
        void tryLoad(String name, IOSupplier<ShaderInstance> create, Consumer<@Nullable ShaderInstance> accept) throws IOException;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerShaders(ResourceProvider resources, ShaderLoader load) throws IOException {
 | 
			
		||||
        load.tryLoad("monitor shader", () -> new MonitorTextureBufferShader(
 | 
			
		||||
                resources,
 | 
			
		||||
                ComputerCraftAPI.MOD_ID + "/monitor_tbo",
 | 
			
		||||
                MONITOR_TBO.format()
 | 
			
		||||
 
 | 
			
		||||
@@ -6,129 +6,69 @@ 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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,7 @@ import net.minecraft.util.CommonColors;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.HitResult;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
 | 
			
		||||
    public static final ResourceLocation COLOUR_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
 | 
			
		||||
@@ -63,8 +62,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();
 | 
			
		||||
        }
 | 
			
		||||
@@ -124,7 +123,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        transform.popPose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, @Nullable int[] tints) {
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
@@ -140,7 +139,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
     * @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, @Nullable int[] tints) {
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,12 +31,12 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
 | 
			
		||||
import net.minecraft.world.phys.AABB;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
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 javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,12 @@ import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
import org.lwjgl.opengl.GL11;
 | 
			
		||||
import org.lwjgl.opengl.GL15;
 | 
			
		||||
import org.lwjgl.opengl.GL30;
 | 
			
		||||
import org.lwjgl.opengl.GL31;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,12 +14,12 @@ 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 javax.annotation.Nullable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
    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 float Z_OFFSET = 1e-3f;
 | 
			
		||||
    private static final float Z_OFFSET = 1e-4f;
 | 
			
		||||
 | 
			
		||||
    private FixedWidthFontRenderer() {
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,9 +10,9 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.sounds.AudioStream;
 | 
			
		||||
import net.minecraft.client.sounds.SoundEngine;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
import org.lwjgl.BufferUtils;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import javax.sound.sampled.AudioFormat;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.ByteOrder;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,7 @@ import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
 | 
			
		||||
@@ -25,7 +24,7 @@ public class SpeakerInstance {
 | 
			
		||||
    SpeakerInstance() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void pushAudio(EncodedAudio buffer) {
 | 
			
		||||
    private void pushAudio(EncodedAudio buffer, float volume) {
 | 
			
		||||
        var sound = this.sound;
 | 
			
		||||
 | 
			
		||||
        var stream = currentStream;
 | 
			
		||||
@@ -33,18 +32,30 @@ public class SpeakerInstance {
 | 
			
		||||
        var exhausted = stream.isEmpty();
 | 
			
		||||
        stream.push(buffer);
 | 
			
		||||
 | 
			
		||||
        // If we've got nothing left in the buffer, enqueue an additional one just in case.
 | 
			
		||||
        if (exhausted && sound != null && sound.stream == stream && stream.channel != null && stream.executor != null) {
 | 
			
		||||
        if (sound == null) return;
 | 
			
		||||
 | 
			
		||||
        var volumeChanged = sound.setVolume(volume);
 | 
			
		||||
 | 
			
		||||
        if ((exhausted || volumeChanged) && sound.stream == stream && stream.channel != null && stream.executor != null) {
 | 
			
		||||
            var actualStream = sound.stream;
 | 
			
		||||
            stream.executor.execute(() -> {
 | 
			
		||||
                var channel = Nullability.assertNonNull(actualStream.channel);
 | 
			
		||||
                if (!channel.stopped()) channel.pumpBuffers(1);
 | 
			
		||||
                if (channel.stopped()) return;
 | 
			
		||||
 | 
			
		||||
                // If we've got nothing left in the buffer, enqueue an additional one just in case.
 | 
			
		||||
                if (exhausted) channel.pumpBuffers(1);
 | 
			
		||||
 | 
			
		||||
                // 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());
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void playAudio(SpeakerPosition position, float volume, EncodedAudio buffer) {
 | 
			
		||||
        pushAudio(buffer);
 | 
			
		||||
        pushAudio(buffer, volume);
 | 
			
		||||
 | 
			
		||||
        var soundManager = Minecraft.getInstance().getSoundManager();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,8 @@ import net.minecraft.client.sounds.SoundBufferLibrary;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundSource;
 | 
			
		||||
import net.minecraft.world.entity.Entity;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -83,4 +83,10 @@ public class SpeakerSound extends AbstractSoundInstance implements TickableSound
 | 
			
		||||
    public @Nullable AudioStream getStream() {
 | 
			
		||||
        return stream;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    boolean setVolume(float volume) {
 | 
			
		||||
        if (volume == this.volume) return false;
 | 
			
		||||
        this.volume = volume;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ 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.jetbrains.annotations.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import com.mojang.serialization.Codec;
 | 
			
		||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.client.gui.GuiSprites;
 | 
			
		||||
import dan200.computercraft.client.model.LecternPocketModel;
 | 
			
		||||
import dan200.computercraft.client.model.LecternPrintoutModel;
 | 
			
		||||
import dan200.computercraft.data.client.ExtraModelsProvider;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
@@ -71,9 +72,11 @@ public final class DataProviders {
 | 
			
		||||
            out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
 | 
			
		||||
                UpgradeSlot.LEFT_UPGRADE,
 | 
			
		||||
                UpgradeSlot.RIGHT_UPGRADE,
 | 
			
		||||
                LecternPrintoutModel.TEXTURE
 | 
			
		||||
                LecternPrintoutModel.TEXTURE,
 | 
			
		||||
                LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
 | 
			
		||||
                LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
 | 
			
		||||
            )));
 | 
			
		||||
            out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
 | 
			
		||||
            out.accept(ResourceLocation.withDefaultNamespace("gui"), makeSprites(
 | 
			
		||||
                // Computers
 | 
			
		||||
                GuiSprites.COMPUTER_NORMAL.textures(),
 | 
			
		||||
                GuiSprites.COMPUTER_ADVANCED.textures(),
 | 
			
		||||
@@ -82,6 +85,8 @@ public final class DataProviders {
 | 
			
		||||
            ));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        generator.add(ResourceMetadataProvider::new);
 | 
			
		||||
 | 
			
		||||
        generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) {
 | 
			
		||||
            @Override
 | 
			
		||||
            public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,8 @@ public final class LanguageProvider implements DataProvider {
 | 
			
		||||
        add(ComputerCraftTags.Items.TURTLE, "Turtles");
 | 
			
		||||
        add(ComputerCraftTags.Items.WIRED_MODEM, "Wired modems");
 | 
			
		||||
        add(ComputerCraftTags.Items.MONITOR, "Monitors");
 | 
			
		||||
        add(ComputerCraftTags.Items.DISKS, "Disks");
 | 
			
		||||
        add(ComputerCraftTags.Items.POCKET_COMPUTERS, "Pocket Computers");
 | 
			
		||||
        add(ComputerCraftTags.Items.DYEABLE, "Dyable items");
 | 
			
		||||
        add(ComputerCraftTags.Items.TURTLE_CAN_PLACE, "Turtle-placeable items");
 | 
			
		||||
 | 
			
		||||
@@ -187,7 +189,6 @@ public final class LanguageProvider implements DataProvider {
 | 
			
		||||
        // Metrics
 | 
			
		||||
        add(Metrics.COMPUTER_TASKS, "Tasks");
 | 
			
		||||
        add(Metrics.SERVER_TASKS, "Server tasks");
 | 
			
		||||
        add(Metrics.JAVA_ALLOCATION, "Java Allocations");
 | 
			
		||||
        add(Metrics.PERIPHERAL_OPS, "Peripheral calls");
 | 
			
		||||
        add(Metrics.FS_OPS, "Filesystem operations");
 | 
			
		||||
        add(Metrics.HTTP_REQUESTS, "HTTP requests");
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,110 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonElement;
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import dan200.computercraft.client.gui.GuiSprites;
 | 
			
		||||
import net.minecraft.client.resources.metadata.gui.GuiMetadataSection;
 | 
			
		||||
import net.minecraft.client.resources.metadata.gui.GuiSpriteScaling;
 | 
			
		||||
import net.minecraft.data.CachedOutput;
 | 
			
		||||
import net.minecraft.data.DataProvider;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.data.metadata.PackMetadataGenerator;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.metadata.MetadataSectionType;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Similar to {@link PackMetadataGenerator}, but for individual resources.
 | 
			
		||||
 */
 | 
			
		||||
final class ResourceMetadataProvider implements DataProvider {
 | 
			
		||||
    private final PackOutput output;
 | 
			
		||||
 | 
			
		||||
    ResourceMetadataProvider(PackOutput output) {
 | 
			
		||||
        this.output = output;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void register(Builder builder) {
 | 
			
		||||
        for (var computerTextures : List.of(
 | 
			
		||||
            GuiSprites.COMPUTER_ADVANCED,
 | 
			
		||||
            GuiSprites.COMPUTER_COLOUR,
 | 
			
		||||
            GuiSprites.COMPUTER_COMMAND,
 | 
			
		||||
            GuiSprites.COMPUTER_NORMAL
 | 
			
		||||
        )) {
 | 
			
		||||
            builder.texture(computerTextures.border()).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
 | 
			
		||||
                new GuiSpriteScaling.NineSlice(36, 36, simpleNineSlicedBorder(12))
 | 
			
		||||
            ));
 | 
			
		||||
 | 
			
		||||
            var sidebar = computerTextures.sidebar();
 | 
			
		||||
            if (sidebar != null) {
 | 
			
		||||
                builder.texture(sidebar).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
 | 
			
		||||
                    new GuiSpriteScaling.NineSlice(17, 14, new GuiSpriteScaling.NineSlice.Border(3, 4, 0, 3))
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var pocketBottom = computerTextures.pocketBottom();
 | 
			
		||||
            if (pocketBottom != null) {
 | 
			
		||||
                builder.texture(pocketBottom).add(GuiMetadataSection.TYPE, new GuiMetadataSection(
 | 
			
		||||
                    new GuiSpriteScaling.NineSlice(36, 20, new GuiSpriteScaling.NineSlice.Border(12, 0, 12, 0))
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static GuiSpriteScaling.NineSlice.Border simpleNineSlicedBorder(int size) {
 | 
			
		||||
        return new GuiSpriteScaling.NineSlice.Border(size, size, size, size);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public CompletableFuture<?> run(CachedOutput cachedOutput) {
 | 
			
		||||
        var builder = new Builder();
 | 
			
		||||
        register(builder);
 | 
			
		||||
 | 
			
		||||
        var outputPath = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK);
 | 
			
		||||
        return CompletableFuture.allOf(builder.metadata.entrySet().stream().map(entry -> {
 | 
			
		||||
            var json = new JsonObject();
 | 
			
		||||
            entry.getValue().elements.forEach((name, element) -> json.add(name, element.get()));
 | 
			
		||||
            return DataProvider.saveStable(cachedOutput, json, outputPath.resolve(entry.getKey().getNamespace()).resolve(entry.getKey().getPath() + ".mcmeta"));
 | 
			
		||||
        }).toArray(CompletableFuture[]::new));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getName() {
 | 
			
		||||
        return "Resource Metadata";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A builder for a set of {@code mcmeta} files.
 | 
			
		||||
     */
 | 
			
		||||
    private static final class Builder {
 | 
			
		||||
        private final Map<ResourceLocation, FileMetadata> metadata = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
        FileMetadata texture(ResourceLocation texture) {
 | 
			
		||||
            return file(texture.withPrefix("textures/").withSuffix(".png"));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        FileMetadata file(ResourceLocation path) {
 | 
			
		||||
            return metadata.computeIfAbsent(path, p -> new FileMetadata());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A builder for a given file's {@code mcmeta} file.
 | 
			
		||||
     */
 | 
			
		||||
    private static final class FileMetadata {
 | 
			
		||||
        private final Map<String, Supplier<JsonElement>> elements = new HashMap<>();
 | 
			
		||||
 | 
			
		||||
        <T> FileMetadata add(MetadataSectionType<T> type, T value) {
 | 
			
		||||
            elements.put(type.getMetadataSectionName(), () -> type.toJson(value));
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -96,6 +96,8 @@ class TagProvider {
 | 
			
		||||
        tags.copy(ComputerCraftTags.Blocks.TURTLE, ComputerCraftTags.Items.TURTLE);
 | 
			
		||||
        tags.tag(ComputerCraftTags.Items.WIRED_MODEM).add(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get());
 | 
			
		||||
        tags.copy(ComputerCraftTags.Blocks.MONITOR, ComputerCraftTags.Items.MONITOR);
 | 
			
		||||
        tags.tag(ComputerCraftTags.Items.DISKS).add(ModRegistry.Items.DISK.get(), ModRegistry.Items.TREASURE_DISK.get());
 | 
			
		||||
        tags.tag(ComputerCraftTags.Items.POCKET_COMPUTERS).add(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
 | 
			
		||||
 | 
			
		||||
        tags.tag(ComputerCraftTags.Items.DYEABLE)
 | 
			
		||||
            .addTag(ComputerCraftTags.Items.TURTLE)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import dan200.computercraft.api.lua.Coerced;
 | 
			
		||||
import dan200.computercraft.api.lua.ILuaAPI;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An example API that will be available on every turtle. This demonstrates both registering an API, and how to write
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,9 @@ package com.example.examplemod;
 | 
			
		||||
import com.example.examplemod.data.TurtleUpgradeProvider;
 | 
			
		||||
import com.example.examplemod.peripheral.FurnacePeripheral;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.detail.VanillaDetailRegistries;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeType;
 | 
			
		||||
import net.minecraft.core.component.DataComponents;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Our example mod, containing the various things we register.
 | 
			
		||||
@@ -34,6 +36,16 @@ public final class ExampleMod {
 | 
			
		||||
        ComputerCraftAPI.registerGenericSource(new FurnacePeripheral());
 | 
			
		||||
        // @end region=generic_source
 | 
			
		||||
 | 
			
		||||
        // @start region=details
 | 
			
		||||
        VanillaDetailRegistries.ITEM_STACK.addProvider((out, stack) -> {
 | 
			
		||||
            var food = stack.get(DataComponents.FOOD);
 | 
			
		||||
            if (food == null) return;
 | 
			
		||||
 | 
			
		||||
            out.put("saturation", food.saturation());
 | 
			
		||||
            out.put("nutrition", food.nutrition());
 | 
			
		||||
        });
 | 
			
		||||
        // @end region=details
 | 
			
		||||
 | 
			
		||||
        ExampleAPI.register();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
package com.example.examplemod;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.detail.DetailRegistry;
 | 
			
		||||
import dan200.computercraft.api.detail.VanillaDetailRegistries;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.api.pocket.IPocketAccess;
 | 
			
		||||
import net.minecraft.world.InteractionHand;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An example peripheral for pocket computers. This currently doesn't have an associated upgrade — it mostly exists to
 | 
			
		||||
 * demonstrate other functionality.
 | 
			
		||||
 */
 | 
			
		||||
public class ExamplePocketPeripheral implements IPeripheral {
 | 
			
		||||
    private final IPocketAccess pocket;
 | 
			
		||||
 | 
			
		||||
    public ExamplePocketPeripheral(IPocketAccess pocket) {
 | 
			
		||||
        this.pocket = pocket;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getType() {
 | 
			
		||||
        return "example";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * An example of using {@linkplain DetailRegistry detail registries} to get the current player's held item.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The item details, or {@code null} if the player is not holding an item.
 | 
			
		||||
     */
 | 
			
		||||
    // @start region=details
 | 
			
		||||
    @LuaFunction(mainThread = true)
 | 
			
		||||
    public final @Nullable Map<String, ?> getHeldItem() {
 | 
			
		||||
        if (!(pocket.getEntity() instanceof LivingEntity entity)) return null;
 | 
			
		||||
 | 
			
		||||
        var heldItem = entity.getItemInHand(InteractionHand.MAIN_HAND);
 | 
			
		||||
        return heldItem.isEmpty() ? null : VanillaDetailRegistries.ITEM_STACK.getDetails(heldItem);
 | 
			
		||||
    }
 | 
			
		||||
    // @end region=details
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean equals(@Nullable IPeripheral other) {
 | 
			
		||||
        return other instanceof ExamplePocketPeripheral o && pocket == o.pocket;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,7 @@ package com.example.examplemod.peripheral;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A peripheral that adds a {@code getFuel()} method to brewing stands. This demonstrates the usage of
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.api.peripheral.AttachedComputerSet;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IComputerAccess;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A peripheral that tracks what computers it is attached to.
 | 
			
		||||
 
 | 
			
		||||
@@ -205,8 +205,10 @@
 | 
			
		||||
  "item.computercraft.treasure_disk": "Floppy Disk",
 | 
			
		||||
  "itemGroup.computercraft": "ComputerCraft",
 | 
			
		||||
  "tag.item.computercraft.computer": "Computers",
 | 
			
		||||
  "tag.item.computercraft.disks": "Disks",
 | 
			
		||||
  "tag.item.computercraft.dyeable": "Dyable items",
 | 
			
		||||
  "tag.item.computercraft.monitor": "Monitors",
 | 
			
		||||
  "tag.item.computercraft.pocket_computers": "Pocket Computers",
 | 
			
		||||
  "tag.item.computercraft.turtle": "Turtles",
 | 
			
		||||
  "tag.item.computercraft.turtle_can_place": "Turtle-placeable items",
 | 
			
		||||
  "tag.item.computercraft.wired_modem": "Wired modems",
 | 
			
		||||
@@ -217,7 +219,6 @@
 | 
			
		||||
  "tracking_field.computercraft.http_download.name": "HTTP download",
 | 
			
		||||
  "tracking_field.computercraft.http_requests.name": "HTTP requests",
 | 
			
		||||
  "tracking_field.computercraft.http_upload.name": "HTTP upload",
 | 
			
		||||
  "tracking_field.computercraft.java_allocation.name": "Java Allocations",
 | 
			
		||||
  "tracking_field.computercraft.max": "%s (max)",
 | 
			
		||||
  "tracking_field.computercraft.peripheral.name": "Peripheral calls",
 | 
			
		||||
  "tracking_field.computercraft.server_tasks.name": "Server tasks",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_advanced.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_advanced.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": 12,
 | 
			
		||||
      "height": 36,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_colour.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_colour.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": 12,
 | 
			
		||||
      "height": 36,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_command.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_command.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": 12,
 | 
			
		||||
      "height": 36,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_normal.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/border_normal.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": 12,
 | 
			
		||||
      "height": 36,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": {
 | 
			
		||||
        "bottom": 0,
 | 
			
		||||
        "left": 12,
 | 
			
		||||
        "right": 12,
 | 
			
		||||
        "top": 0
 | 
			
		||||
      },
 | 
			
		||||
      "height": 20,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": {
 | 
			
		||||
        "bottom": 0,
 | 
			
		||||
        "left": 12,
 | 
			
		||||
        "right": 12,
 | 
			
		||||
        "top": 0
 | 
			
		||||
      },
 | 
			
		||||
      "height": 20,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": {
 | 
			
		||||
        "bottom": 0,
 | 
			
		||||
        "left": 12,
 | 
			
		||||
        "right": 12,
 | 
			
		||||
        "top": 0
 | 
			
		||||
      },
 | 
			
		||||
      "height": 20,
 | 
			
		||||
      "width": 36
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": {
 | 
			
		||||
        "bottom": 3,
 | 
			
		||||
        "left": 3,
 | 
			
		||||
        "right": 0,
 | 
			
		||||
        "top": 4
 | 
			
		||||
      },
 | 
			
		||||
      "height": 14,
 | 
			
		||||
      "width": 17
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_command.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_command.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": {
 | 
			
		||||
        "bottom": 3,
 | 
			
		||||
        "left": 3,
 | 
			
		||||
        "right": 0,
 | 
			
		||||
        "top": 4
 | 
			
		||||
      },
 | 
			
		||||
      "height": 14,
 | 
			
		||||
      "width": 17
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_normal.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								projects/common/src/generated/resources/assets/computercraft/textures/gui/sidebar_normal.png.mcmeta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "gui": {
 | 
			
		||||
    "scaling": {
 | 
			
		||||
      "type": "nine_slice",
 | 
			
		||||
      "border": {
 | 
			
		||||
        "bottom": 3,
 | 
			
		||||
        "left": 3,
 | 
			
		||||
        "right": 0,
 | 
			
		||||
        "top": 4
 | 
			
		||||
      },
 | 
			
		||||
      "height": 14,
 | 
			
		||||
      "width": 17
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user