mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-03 23:22:59 +00:00 
			
		
		
		
	Compare commits
	
		
			26 Commits
		
	
	
		
			v1.21.1-1.
			...
			v1.21.4-1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9277aa33e9 | ||
| 
						 | 
					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 | 
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,7 @@
 | 
			
		||||
 | 
			
		||||
/** Default configuration for Fabric projects. */
 | 
			
		||||
 | 
			
		||||
import cc.tweaked.gradle.CCTweakedExtension
 | 
			
		||||
import cc.tweaked.gradle.CCTweakedPlugin
 | 
			
		||||
import cc.tweaked.gradle.IdeaRunConfigurations
 | 
			
		||||
import cc.tweaked.gradle.MinecraftConfigurations
 | 
			
		||||
import cc.tweaked.gradle.*
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
    `java-library`
 | 
			
		||||
@@ -67,3 +64,9 @@ dependencies {
 | 
			
		||||
tasks.ideaSyncTask {
 | 
			
		||||
    doLast { IdeaRunConfigurations(project).patch() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.named("checkDependencyConsistency", DependencyCheck::class.java) {
 | 
			
		||||
    val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
 | 
			
		||||
    // Minecraft depends on asm, but Fabric forces it to a more recent version
 | 
			
		||||
    override(libs.findLibrary("asm").get(), "9.7.1")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +91,6 @@ 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("ReferenceEquality", CheckSeverity.OFF)
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -126,7 +129,7 @@ SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
        <module name="LocalFinalVariableName" />
 | 
			
		||||
        <module name="LocalVariableName" />
 | 
			
		||||
        <module name="MemberName">
 | 
			
		||||
            <property name="format" value="^\$?[a-z][a-zA-Z0-9]*$" />
 | 
			
		||||
            <property name="format" value="^(computercraft\$|\$)?[a-z][a-zA-Z0-9]*$" />
 | 
			
		||||
        </module>
 | 
			
		||||
        <module name="MethodName">
 | 
			
		||||
            <property name="format" value="^(computercraft\$)?[a-z][a-zA-Z0-9]*$" />
 | 
			
		||||
@@ -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.115.1
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.21.1
 | 
			
		||||
mcVersion=1.21.4
 | 
			
		||||
 
 | 
			
		||||
@@ -7,47 +7,47 @@
 | 
			
		||||
# Minecraft
 | 
			
		||||
# MC version is specified in gradle.properties, as we need that in settings.gradle.
 | 
			
		||||
# Remember to update corresponding versions in fabric.mod.json/neoforge.mods.toml
 | 
			
		||||
fabric-api = "0.102.1+1.21.1"
 | 
			
		||||
fabric-loader = "0.15.11"
 | 
			
		||||
neoForge = "21.1.9"
 | 
			
		||||
fabric-api = "0.118.0+1.21.4"
 | 
			
		||||
fabric-loader = "0.16.10"
 | 
			
		||||
neoForge = "21.4.101-beta"
 | 
			
		||||
neoForgeSpi = "8.0.1"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2024.07.28"
 | 
			
		||||
parchmentMc = "1.21"
 | 
			
		||||
yarn = "1.21.1+build.1"
 | 
			
		||||
parchment = "2024.12.07"
 | 
			
		||||
parchmentMc = "1.21.4"
 | 
			
		||||
yarn = "1.21.4+build.1"
 | 
			
		||||
 | 
			
		||||
# Core dependencies (these versions are tied to the version Minecraft uses)
 | 
			
		||||
fastutil = "8.5.12"
 | 
			
		||||
guava = "32.1.2-jre"
 | 
			
		||||
netty = "4.1.97.Final"
 | 
			
		||||
slf4j = "2.0.9"
 | 
			
		||||
fastutil = "8.5.15"
 | 
			
		||||
guava = "33.3.1-jre"
 | 
			
		||||
netty = "4.1.115.Final"
 | 
			
		||||
slf4j = "2.0.16"
 | 
			
		||||
 | 
			
		||||
# Core dependencies (independent of Minecraft)
 | 
			
		||||
asm = "9.6"
 | 
			
		||||
asm = "9.7.1"
 | 
			
		||||
autoService = "1.1.1"
 | 
			
		||||
checkerFramework = "3.42.0"
 | 
			
		||||
cobalt = { strictly = "0.9.5" }
 | 
			
		||||
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"
 | 
			
		||||
 | 
			
		||||
# Minecraft mods
 | 
			
		||||
emi = "1.1.7+1.21"
 | 
			
		||||
fabricPermissions = "0.3.1"
 | 
			
		||||
iris-fabric = "1.8.0-beta.3+1.21-fabric"
 | 
			
		||||
iris-forge = "1.8.0-beta.3+1.21-neoforge"
 | 
			
		||||
fabricPermissions = "0.3.3"
 | 
			
		||||
iris-fabric = "1.8.8+1.21.4-fabric"
 | 
			
		||||
iris-forge = "1.8.8+1.21.4-neoforge"
 | 
			
		||||
jei = "19.8.2.99"
 | 
			
		||||
modmenu = "11.0.0-rc.4"
 | 
			
		||||
moreRed = "4.0.0.4"
 | 
			
		||||
rei = "16.0.729"
 | 
			
		||||
sodium-fabric = "mc1.21-0.6.0-beta.1-fabric"
 | 
			
		||||
sodium-forge = "mc1.21-0.6.0-beta.1-neoforge"
 | 
			
		||||
modmenu = "13.0.2"
 | 
			
		||||
moreRed = "6.0.0.3"
 | 
			
		||||
rei = "18.0.800"
 | 
			
		||||
sodium-fabric = "mc1.21.4-0.6.10-fabric"
 | 
			
		||||
sodium-forge = "mc1.21.4-0.6.10-neoforge"
 | 
			
		||||
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
 | 
			
		||||
@@ -59,9 +59,9 @@ jmh = "1.37"
 | 
			
		||||
 | 
			
		||||
# Build tools
 | 
			
		||||
cctJavadoc = "1.8.3"
 | 
			
		||||
checkstyle = "10.20.1"
 | 
			
		||||
errorProne-core = "2.27.0"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
checkstyle = "10.21.2"
 | 
			
		||||
errorProne-core = "2.36.0"
 | 
			
		||||
errorProne-plugin = "4.1.0"
 | 
			
		||||
fabric-loom = "1.9.2"
 | 
			
		||||
githubRelease = "2.5.2"
 | 
			
		||||
gradleVersions = "0.50.0"
 | 
			
		||||
@@ -70,7 +70,7 @@ illuaminate = "0.1.0-74-gf1551d5"
 | 
			
		||||
lwjgl = "3.3.3"
 | 
			
		||||
minotaur = "2.8.7"
 | 
			
		||||
modDevGradle = "2.0.74"
 | 
			
		||||
nullAway = "0.10.25"
 | 
			
		||||
nullAway = "0.12.3"
 | 
			
		||||
shadow = "8.3.1"
 | 
			
		||||
spotless = "6.23.3"
 | 
			
		||||
taskTree = "2.1.1"
 | 
			
		||||
@@ -90,7 +90,7 @@ fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
 | 
			
		||||
neoForgeSpi = { module = "net.neoforged:neoforgespi", version.ref = "neoForgeSpi" }
 | 
			
		||||
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,15 +180,15 @@ 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
 | 
			
		||||
externalMods-common = ["iris-forge", "jei-api", "nightConfig-core", "nightConfig-toml"]
 | 
			
		||||
externalMods-forge-compile = ["moreRed", "iris-forge", "jei-api"]
 | 
			
		||||
externalMods-forge-runtime = ["jei-forge"]
 | 
			
		||||
externalMods-forge-runtime = []
 | 
			
		||||
externalMods-fabric-compile = ["fabricPermissions", "iris-fabric", "jei-api", "rei-api", "rei-builtin"]
 | 
			
		||||
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
 | 
			
		||||
externalMods-fabric-runtime = []
 | 
			
		||||
 | 
			
		||||
# Testing
 | 
			
		||||
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelManager;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The location of a model to load. This may either be:
 | 
			
		||||
 *
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *     <li>A {@link ModelResourceLocation}, referencing an already baked model (such as {@code minecraft:dirt#inventory}).</li>
 | 
			
		||||
 *     <li>
 | 
			
		||||
 *         A {@link ResourceLocation}, referencing a path to a model resource (such as {@code minecraft:item/dirt}.
 | 
			
		||||
 *         These models will be baked and stored in the {@link ModelManager} in a loader-specific way.
 | 
			
		||||
 *     </li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 */
 | 
			
		||||
public final class ModelLocation {
 | 
			
		||||
    /**
 | 
			
		||||
     * The location of the model.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * When {@link #resourceLocation} is null, this is the location of the model to load. When {@link #resourceLocation}
 | 
			
		||||
     * is non-null, this is the "standalone" variant of the model resource — this is used by NeoForge's implementation
 | 
			
		||||
     * of {@link ClientPlatformHelper#getModel(ModelManager, ModelResourceLocation, ResourceLocation)} to fetch the
 | 
			
		||||
     * model from the model manger. It is not used on Fabric.
 | 
			
		||||
     */
 | 
			
		||||
    private final ModelResourceLocation modelLocation;
 | 
			
		||||
    private final @Nullable ResourceLocation resourceLocation;
 | 
			
		||||
 | 
			
		||||
    private ModelLocation(ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation) {
 | 
			
		||||
        this.modelLocation = modelLocation;
 | 
			
		||||
        this.resourceLocation = resourceLocation;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link ModelLocation} from model in the model manager.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The name of the model to load.
 | 
			
		||||
     * @return The new {@link ModelLocation} instance.
 | 
			
		||||
     */
 | 
			
		||||
    public static ModelLocation ofModel(ModelResourceLocation location) {
 | 
			
		||||
        return new ModelLocation(location, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link ModelLocation} from a resource.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The location of the model resource, such as {@code minecraft:item/dirt}.
 | 
			
		||||
     * @return The new {@link ModelLocation} instance.
 | 
			
		||||
     */
 | 
			
		||||
    public static ModelLocation ofResource(ResourceLocation location) {
 | 
			
		||||
        return new ModelLocation(new ModelResourceLocation(location, "standalone"), location);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get this model from the provided model manager.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manager The model manger.
 | 
			
		||||
     * @return This model, or the missing model if it could not be found.
 | 
			
		||||
     */
 | 
			
		||||
    public BakedModel getModel(ModelManager manager) {
 | 
			
		||||
        return ClientPlatformHelper.get().getModel(manager, modelLocation, resourceLocation);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the models this model location depends on.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A list of models that this model location depends on.
 | 
			
		||||
     * @see TurtleUpgradeModeller#getDependencies()
 | 
			
		||||
     */
 | 
			
		||||
    public Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
        return resourceLocation == null ? Stream.empty() : Stream.of(resourceLocation);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,19 +9,21 @@ import com.mojang.math.Transformation;
 | 
			
		||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A model to render, combined with a transformation matrix to apply.
 | 
			
		||||
 *
 | 
			
		||||
 * @param model  The model.
 | 
			
		||||
 * @param matrix The transformation matrix.
 | 
			
		||||
 */
 | 
			
		||||
public record TransformedModel(BakedModel model, Transformation matrix) {
 | 
			
		||||
    public TransformedModel(BakedModel model) {
 | 
			
		||||
        this(model, Transformation.identity());
 | 
			
		||||
public sealed interface TransformedModel permits TransformedModel.Baked, TransformedModel.Item {
 | 
			
		||||
    record Baked(BakedModel model) implements TransformedModel {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    record Item(ItemStack stack, Transformation transformation) implements TransformedModel {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static TransformedModel of(BakedModel model) {
 | 
			
		||||
        return new TransformedModel.Baked(model);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -30,37 +32,12 @@ public record TransformedModel(BakedModel model, Transformation matrix) {
 | 
			
		||||
     * @param location The location of the model to load.
 | 
			
		||||
     * @return The new {@link TransformedModel} instance.
 | 
			
		||||
     */
 | 
			
		||||
    public static TransformedModel of(ModelLocation location) {
 | 
			
		||||
    static TransformedModel of(ResourceLocation location) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        return new TransformedModel(location.getModel(modelManager));
 | 
			
		||||
        return of(ClientPlatformHelper.get().getModel(modelManager, location));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The location of the model to load.
 | 
			
		||||
     * @return The new {@link TransformedModel} instance.
 | 
			
		||||
     * @see ModelLocation#ofModel(ModelResourceLocation)
 | 
			
		||||
     */
 | 
			
		||||
    public static TransformedModel of(ModelResourceLocation location) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        return new TransformedModel(modelManager.getModel(location));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The location of the model to load.
 | 
			
		||||
     * @return The new {@link TransformedModel} instance.
 | 
			
		||||
     * @see ModelLocation#ofResource(ResourceLocation)
 | 
			
		||||
     */
 | 
			
		||||
    public static TransformedModel of(ResourceLocation location) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        return new TransformedModel(ClientPlatformHelper.get().getModel(modelManager, location));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TransformedModel of(ItemStack item, Transformation transform) {
 | 
			
		||||
        var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(item);
 | 
			
		||||
        return new TransformedModel(model, transform);
 | 
			
		||||
    static TransformedModel of(ItemStack item, Transformation transform) {
 | 
			
		||||
        return new TransformedModel.Item(item, transform);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.client.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.client.ModelLocation;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
@@ -12,8 +11,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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -55,7 +54,7 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
 | 
			
		||||
     * by other means.
 | 
			
		||||
     *
 | 
			
		||||
     * @return A list of models that this modeller depends on.
 | 
			
		||||
     * @see UnbakedModel#getDependencies()
 | 
			
		||||
     * @see UnbakedModel#resolveDependencies(UnbakedModel.Resolver)
 | 
			
		||||
     */
 | 
			
		||||
    default Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
        return Stream.of();
 | 
			
		||||
@@ -85,18 +84,6 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
 | 
			
		||||
     * @return The constructed modeller.
 | 
			
		||||
     */
 | 
			
		||||
    static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ResourceLocation left, ResourceLocation right) {
 | 
			
		||||
        return sided(ModelLocation.ofResource(left), ModelLocation.ofResource(right));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Construct a {@link TurtleUpgradeModeller} which has a single model for the left and right side.
 | 
			
		||||
     *
 | 
			
		||||
     * @param left  The model to use on the left.
 | 
			
		||||
     * @param right The model to use on the right.
 | 
			
		||||
     * @param <T>   The type of the turtle upgrade.
 | 
			
		||||
     * @return The constructed modeller.
 | 
			
		||||
     */
 | 
			
		||||
    static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ModelLocation left, ModelLocation right) {
 | 
			
		||||
        return new TurtleUpgradeModeller<>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
 | 
			
		||||
@@ -105,7 +92,7 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
                return Stream.of(left, right).flatMap(ModelLocation::getDependencies);
 | 
			
		||||
                return Stream.of(left, right);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,32 +4,27 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.client.turtle;
 | 
			
		||||
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import com.mojang.math.Transformation;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import 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);
 | 
			
		||||
    private static final Transformation rightTransform = getMatrixFor(0.4065f);
 | 
			
		||||
    private static final Transformation leftTransform = getMatrixFor(TurtleSide.LEFT);
 | 
			
		||||
    private static final Transformation rightTransform = getMatrixFor(TurtleSide.RIGHT);
 | 
			
		||||
 | 
			
		||||
    private static Transformation getMatrixFor(float offset) {
 | 
			
		||||
        var matrix = new Matrix4f();
 | 
			
		||||
        matrix.set(new float[]{
 | 
			
		||||
            0.0f, 0.0f, -1.0f, 1.0f + offset,
 | 
			
		||||
            1.0f, 0.0f, 0.0f, 0.0f,
 | 
			
		||||
            0.0f, -1.0f, 0.0f, 1.0f,
 | 
			
		||||
            0.0f, 0.0f, 0.0f, 1.0f,
 | 
			
		||||
        });
 | 
			
		||||
        matrix.transpose();
 | 
			
		||||
        return new Transformation(matrix);
 | 
			
		||||
    private static Transformation getMatrixFor(TurtleSide side) {
 | 
			
		||||
        var pose = new Matrix4f();
 | 
			
		||||
        pose.translate(0.5f, 0.5f, 0.5f);
 | 
			
		||||
        pose.rotate(Axis.YN.rotationDegrees(90f));
 | 
			
		||||
        pose.rotate(Axis.ZP.rotationDegrees(90f));
 | 
			
		||||
        pose.translate(0.0f, 0.0f, side == TurtleSide.RIGHT ? -0.4065f : 0.4065f);
 | 
			
		||||
        return new Transformation(pose);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
 | 
			
		||||
@@ -37,10 +32,7 @@ final class TurtleUpgradeModellers {
 | 
			
		||||
    private static final class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
 | 
			
		||||
            var stack = upgrade.getUpgradeItem(data);
 | 
			
		||||
            var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(stack);
 | 
			
		||||
            if (stack.hasFoil()) model = ClientPlatformHelper.get().createdFoiledModel(model);
 | 
			
		||||
            return new TransformedModel(model, side == TurtleSide.LEFT ? leftTransform : rightTransform);
 | 
			
		||||
            return TransformedModel.of(upgrade.getUpgradeItem(data), side == TurtleSide.LEFT ? leftTransform : rightTransform);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,12 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.client.ModelLocation;
 | 
			
		||||
import dan200.computercraft.impl.Services;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelManager;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
@ApiStatus.Internal
 | 
			
		||||
public interface ClientPlatformHelper {
 | 
			
		||||
@@ -23,33 +19,9 @@ public interface ClientPlatformHelper {
 | 
			
		||||
     * @param manager          The model manager.
 | 
			
		||||
     * @param resourceLocation The model resourceLocation.
 | 
			
		||||
     * @return The baked model.
 | 
			
		||||
     * @see ModelLocation
 | 
			
		||||
     */
 | 
			
		||||
    BakedModel getModel(ModelManager manager, ResourceLocation resourceLocation);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set a model from a {@link ModelResourceLocation} or {@link ResourceLocation}.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is largely equivalent to {@code resourceLocation == null ? manager.getModel(modelLocation) : getModel(manager, resourceLocation)},
 | 
			
		||||
     * but allows pre-computing {@code modelLocation} (if needed).
 | 
			
		||||
     *
 | 
			
		||||
     * @param manager          The model manager.
 | 
			
		||||
     * @param modelLocation    The location of the model to load.
 | 
			
		||||
     * @param resourceLocation The location of the resource, if trying to load from a resource.
 | 
			
		||||
     * @return The baked model.
 | 
			
		||||
     * @see ModelLocation
 | 
			
		||||
     */
 | 
			
		||||
    BakedModel getModel(ModelManager manager, ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wrap this model in a version which renders a foil/enchantment glint.
 | 
			
		||||
     *
 | 
			
		||||
     * @param model The model to wrap.
 | 
			
		||||
     * @return The wrapped model.
 | 
			
		||||
     * @see RenderType#glint()
 | 
			
		||||
     */
 | 
			
		||||
    BakedModel createdFoiledModel(BakedModel model);
 | 
			
		||||
 | 
			
		||||
    static ClientPlatformHelper get() {
 | 
			
		||||
        var instance = Instance.INSTANCE;
 | 
			
		||||
        return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@ import dan200.computercraft.api.lua.GenericSource;
 | 
			
		||||
import dan200.computercraft.api.lua.IComputerSystem;
 | 
			
		||||
import dan200.computercraft.api.lua.ILuaAPI;
 | 
			
		||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
 | 
			
		||||
import dan200.computercraft.api.media.IMedia;
 | 
			
		||||
import dan200.computercraft.api.media.MediaProvider;
 | 
			
		||||
import dan200.computercraft.api.network.PacketNetwork;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredElement;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
@@ -26,8 +24,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.
 | 
			
		||||
@@ -143,16 +140,6 @@ public final class ComputerCraftAPI {
 | 
			
		||||
        return getInstance().getBundledRedstoneOutput(world, pos, side);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Registers a media provider to provide {@link IMedia} implementations for Items.
 | 
			
		||||
     *
 | 
			
		||||
     * @param provider The media provider to register.
 | 
			
		||||
     * @see MediaProvider
 | 
			
		||||
     */
 | 
			
		||||
    public static void registerMediaProvider(MediaProvider provider) {
 | 
			
		||||
        getInstance().registerMediaProvider(provider);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Attempt to get the game-wide wireless network.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. This API may be redistributed unmodified and in full only.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.media;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This interface is used to provide {@link IMedia} implementations for {@link ItemStack}.
 | 
			
		||||
 *
 | 
			
		||||
 * @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(MediaProvider)
 | 
			
		||||
 */
 | 
			
		||||
@FunctionalInterface
 | 
			
		||||
public interface MediaProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Produce an IMedia implementation from an ItemStack.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The stack from which to extract the media information.
 | 
			
		||||
     * @return An {@link IMedia} implementation, or {@code null} if the item is not something you wish to handle
 | 
			
		||||
     * @see dan200.computercraft.api.ComputerCraftAPI#registerMediaProvider(MediaProvider)
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    IMedia getMedia(ItemStack stack);
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,17 +4,33 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.turtle;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.Codec;
 | 
			
		||||
import net.minecraft.util.StringRepresentable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An enum representing the two sides of the turtle that a turtle upgrade might reside.
 | 
			
		||||
 */
 | 
			
		||||
public enum TurtleSide {
 | 
			
		||||
public enum TurtleSide implements StringRepresentable {
 | 
			
		||||
    /**
 | 
			
		||||
     * The turtle's left side (where the pickaxe usually is on a Wireless Mining Turtle).
 | 
			
		||||
     */
 | 
			
		||||
    LEFT,
 | 
			
		||||
    LEFT("left"),
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The turtle's right side (where the modem usually is on a Wireless Mining Turtle).
 | 
			
		||||
     */
 | 
			
		||||
    RIGHT,
 | 
			
		||||
    RIGHT("right");
 | 
			
		||||
 | 
			
		||||
    public static final Codec<TurtleSide> CODEC = StringRepresentable.fromEnum(TurtleSide::values);
 | 
			
		||||
 | 
			
		||||
    private final String name;
 | 
			
		||||
 | 
			
		||||
    TurtleSide(String name) {
 | 
			
		||||
        this.name = name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getSerializedName() {
 | 
			
		||||
        return name;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ import java.util.function.Function;
 | 
			
		||||
 * @see ITurtleUpgrade
 | 
			
		||||
 * @see IPocketUpgrade
 | 
			
		||||
 */
 | 
			
		||||
public interface UpgradeType<T extends UpgradeBase> {
 | 
			
		||||
public sealed interface UpgradeType<T extends UpgradeBase> permits UpgradeTypeImpl {
 | 
			
		||||
    /**
 | 
			
		||||
     * The codec to read and write this upgrade from a datapack.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import dan200.computercraft.api.filesystem.Mount;
 | 
			
		||||
import dan200.computercraft.api.filesystem.WritableMount;
 | 
			
		||||
import dan200.computercraft.api.lua.GenericSource;
 | 
			
		||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
 | 
			
		||||
import dan200.computercraft.api.media.MediaProvider;
 | 
			
		||||
import dan200.computercraft.api.media.PrintoutContents;
 | 
			
		||||
import dan200.computercraft.api.network.PacketNetwork;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredElement;
 | 
			
		||||
import dan200.computercraft.api.network.wired.WiredNode;
 | 
			
		||||
@@ -30,8 +30,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}
 | 
			
		||||
@@ -60,8 +59,6 @@ public interface ComputerCraftAPIService {
 | 
			
		||||
 | 
			
		||||
    int getBundledRedstoneOutput(Level world, BlockPos pos, Direction side);
 | 
			
		||||
 | 
			
		||||
    void registerMediaProvider(MediaProvider provider);
 | 
			
		||||
 | 
			
		||||
    PacketNetwork getWirelessNetwork(MinecraftServer server);
 | 
			
		||||
 | 
			
		||||
    void registerAPIFactory(ILuaAPIFactory factory);
 | 
			
		||||
@@ -84,6 +81,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>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,13 @@ plugins {
 | 
			
		||||
    id("cc-tweaked.publishing")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sourceSets.client {
 | 
			
		||||
    java {
 | 
			
		||||
        exclude("dan200/computercraft/client/integration/emi")
 | 
			
		||||
        exclude("dan200/computercraft/client/integration/jei")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
minecraft {
 | 
			
		||||
    accessWideners(
 | 
			
		||||
        "src/main/resources/computercraft.accesswidener",
 | 
			
		||||
@@ -39,7 +46,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,13 @@ package dan200.computercraft.client;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.audio.Channel;
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
 | 
			
		||||
import dan200.computercraft.client.render.CableHighlightRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.ExtendedItemFrameRenderState;
 | 
			
		||||
import dan200.computercraft.client.render.PocketItemRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.PrintoutItemRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.monitor.MonitorHighlightRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.monitor.MonitorRenderState;
 | 
			
		||||
import dan200.computercraft.client.sound.SpeakerManager;
 | 
			
		||||
@@ -29,17 +30,17 @@ import dan200.computercraft.shared.util.WorldUtil;
 | 
			
		||||
import net.minecraft.client.Camera;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
 | 
			
		||||
import net.minecraft.client.sounds.AudioStream;
 | 
			
		||||
import net.minecraft.client.sounds.SoundEngine;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.world.InteractionHand;
 | 
			
		||||
import net.minecraft.world.entity.decoration.ItemFrame;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.level.block.state.BlockState;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.HitResult;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -91,9 +92,10 @@ public final class ClientHooks {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int light) {
 | 
			
		||||
        if (stack.getItem() instanceof PrintoutItem) {
 | 
			
		||||
            PrintoutItemRenderer.onRenderInFrame(transform, render, frame, stack, light);
 | 
			
		||||
    public static boolean onRenderItemFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, ExtendedItemFrameRenderState state, int light) {
 | 
			
		||||
        if (state.printoutData != null) {
 | 
			
		||||
            transform.mulPose(Axis.ZP.rotationDegrees(frame.rotation * 360.0f / 8.0f));
 | 
			
		||||
            PrintoutItemRenderer.onRenderInFrame(transform, render, frame, state.printoutData, state.isBook, light);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -135,17 +137,6 @@ public final class ClientHooks {
 | 
			
		||||
        if (upgrade != null) out.accept(String.format("Upgrade[%s]: %s", side, upgrade.holder().key().location()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add additional information about the game to the debug screen.
 | 
			
		||||
     *
 | 
			
		||||
     * @param addText A callback which adds a single line of text.
 | 
			
		||||
     */
 | 
			
		||||
    public static void addGameDebugInfo(Consumer<String> addText) {
 | 
			
		||||
        if (MonitorBlockEntityRenderer.hasRenderedThisFrame() && Minecraft.getInstance().getDebugOverlay().showDebugScreen()) {
 | 
			
		||||
            addText.accept("[CC:T] Monitor renderer: " + MonitorBlockEntityRenderer.currentRenderer());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static @Nullable BlockState getBlockBreakingState(BlockState state, BlockPos pos) {
 | 
			
		||||
        // Only apply to cables which have both a cable and modem
 | 
			
		||||
        if (state.getBlock() != ModRegistry.Blocks.CABLE.get()
 | 
			
		||||
 
 | 
			
		||||
@@ -8,57 +8,47 @@ import com.mojang.brigadier.CommandDispatcher;
 | 
			
		||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
 | 
			
		||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 | 
			
		||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.client.gui.*;
 | 
			
		||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
 | 
			
		||||
import dan200.computercraft.client.item.colour.PocketComputerLight;
 | 
			
		||||
import dan200.computercraft.client.item.model.TurtleOverlayModel;
 | 
			
		||||
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
 | 
			
		||||
import dan200.computercraft.client.item.properties.PocketComputerStateProperty;
 | 
			
		||||
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
 | 
			
		||||
import dan200.computercraft.client.render.CustomLecternRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleModemModeller;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.command.CommandComputerCraft;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ServerContext;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.media.items.DiskItem;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.color.item.ItemColor;
 | 
			
		||||
import net.minecraft.client.color.item.ItemTintSource;
 | 
			
		||||
import net.minecraft.client.gui.screens.MenuScreens;
 | 
			
		||||
import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
 | 
			
		||||
import net.minecraft.client.multiplayer.ClientLevel;
 | 
			
		||||
import net.minecraft.client.renderer.ShaderInstance;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
 | 
			
		||||
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemProperties;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemModel;
 | 
			
		||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
 | 
			
		||||
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceProvider;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
import net.minecraft.world.inventory.MenuType;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.component.DyedItemColor;
 | 
			
		||||
import net.minecraft.world.level.ItemLike;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
 | 
			
		||||
@@ -83,25 +73,6 @@ public final class ClientRegistry {
 | 
			
		||||
        BlockEntityRenderers.register(ModRegistry.BlockEntities.LECTERN.get(), CustomLecternRenderer::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register any client-side objects which must be done on the main thread.
 | 
			
		||||
     *
 | 
			
		||||
     * @param itemProperties Callback to register item properties.
 | 
			
		||||
     */
 | 
			
		||||
    public static void registerMainThread(RegisterItemProperty itemProperties) {
 | 
			
		||||
        registerItemProperty(itemProperties, "state",
 | 
			
		||||
            new UnclampedPropertyFunction((stack, world, player, random) -> {
 | 
			
		||||
                var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
                return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
 | 
			
		||||
            }),
 | 
			
		||||
            ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
 | 
			
		||||
        );
 | 
			
		||||
        registerItemProperty(itemProperties, "coloured",
 | 
			
		||||
            (stack, world, player, random) -> DyedItemColor.getOrDefault(stack, -1) != -1 ? 1 : 0,
 | 
			
		||||
            ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerMenuScreens(RegisterMenuScreen register) {
 | 
			
		||||
        register.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
 | 
			
		||||
        register.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
 | 
			
		||||
@@ -129,26 +100,14 @@ public final class ClientRegistry {
 | 
			
		||||
        register.register(ModRegistry.TurtleUpgradeTypes.TOOL.get(), TurtleUpgradeModeller.flatItem());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SafeVarargs
 | 
			
		||||
    private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
 | 
			
		||||
        var id = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
 | 
			
		||||
        for (var item : items) itemProperties.register(item.get(), id, getter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register an item property via {@link ItemProperties#register}. Forge and Fabric expose different methods, so we
 | 
			
		||||
     * supply this via mod-loader-specific code.
 | 
			
		||||
     */
 | 
			
		||||
    public interface RegisterItemProperty {
 | 
			
		||||
        void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
 | 
			
		||||
        register.accept(GuiSprites.initialise(minecraft.getTextureManager()));
 | 
			
		||||
    public static void registerReloadListeners(BiConsumer<ResourceLocation, PreparableReloadListener> register, Minecraft minecraft) {
 | 
			
		||||
        register.accept(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sprites"), GuiSprites.initialise(minecraft.getTextureManager()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final ResourceLocation[] EXTRA_MODELS = {
 | 
			
		||||
        TurtleOverlay.ELF_MODEL,
 | 
			
		||||
        TurtleBlockEntityRenderer.NORMAL_TURTLE_MODEL,
 | 
			
		||||
        TurtleBlockEntityRenderer.ADVANCED_TURTLE_MODEL,
 | 
			
		||||
        TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -158,56 +117,21 @@ public final class ClientRegistry {
 | 
			
		||||
        TurtleUpgradeModellers.getDependencies().forEach(register);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
 | 
			
		||||
        register.accept(
 | 
			
		||||
            (stack, layer) -> layer == 1 ? DiskItem.getColour(stack) : -1,
 | 
			
		||||
            ModRegistry.Items.DISK.get()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        register.accept(
 | 
			
		||||
            (stack, layer) -> layer == 1 ? DyedItemColor.getOrDefault(stack, Colour.BLUE.getARGB()) : -1,
 | 
			
		||||
            ModRegistry.Items.TREASURE_DISK.get()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get());
 | 
			
		||||
        register.accept(ClientRegistry::getPocketColour, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
 | 
			
		||||
 | 
			
		||||
        register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_NORMAL.get());
 | 
			
		||||
        register.accept(ClientRegistry::getTurtleColour, ModRegistry.Blocks.TURTLE_ADVANCED.get());
 | 
			
		||||
    public static void registerItemModels(BiConsumer<ResourceLocation, MapCodec<? extends ItemModel.Unbaked>> register) {
 | 
			
		||||
        register.accept(TurtleOverlayModel.ID, TurtleOverlayModel.CODEC);
 | 
			
		||||
        register.accept(TurtleUpgradeModel.ID, TurtleUpgradeModel.CODEC);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getPocketColour(ItemStack stack, int layer) {
 | 
			
		||||
        return switch (layer) {
 | 
			
		||||
            default -> -1;
 | 
			
		||||
            case 1 -> DyedItemColor.getOrDefault(stack, -1); // Frame colour
 | 
			
		||||
            case 2 -> { // Light colour
 | 
			
		||||
                var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
                yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getARGB() : FastColor.ARGB32.opaque(computer.getLightState());
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    public static void registerItemColours(BiConsumer<ResourceLocation, MapCodec<? extends ItemTintSource>> register) {
 | 
			
		||||
        register.accept(PocketComputerLight.ID, PocketComputerLight.CODEC);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getTurtleColour(ItemStack stack, int layer) {
 | 
			
		||||
        return layer == 0 ? DyedItemColor.getOrDefault(stack, -1) : -1;
 | 
			
		||||
    public static void registerSelectItemProperties(BiConsumer<ResourceLocation, SelectItemModelProperty.Type<?, ?>> register) {
 | 
			
		||||
        register.accept(PocketComputerStateProperty.ID, PocketComputerStateProperty.TYPE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
 | 
			
		||||
        RenderTypes.registerShaders(resources, load);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record UnclampedPropertyFunction(
 | 
			
		||||
        ClampedItemPropertyFunction function
 | 
			
		||||
    ) implements ClampedItemPropertyFunction {
 | 
			
		||||
        @Override
 | 
			
		||||
        public float unclampedCall(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
 | 
			
		||||
            return function.unclampedCall(stack, level, entity, layer);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Deprecated
 | 
			
		||||
        @Override
 | 
			
		||||
        public float call(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int layer) {
 | 
			
		||||
            return function.unclampedCall(stack, level, entity, layer);
 | 
			
		||||
        }
 | 
			
		||||
    public static void registerConditionalItemProperties(BiConsumer<ResourceLocation, MapCodec<? extends ConditionalItemModelProperty>> register) {
 | 
			
		||||
        register.accept(TurtleShowElfOverlay.ID, TurtleShowElfOverlay.CODEC);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -99,7 +99,7 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
 | 
			
		||||
 | 
			
		||||
        if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
 | 
			
		||||
            new ItemToast(minecraft(), displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
 | 
			
		||||
                .showOrReplace(minecraft().getToasts());
 | 
			
		||||
                .showOrReplace(minecraft().getToastManager());
 | 
			
		||||
            uploadNagDeadline = Long.MAX_VALUE;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ 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.shared.computer.inventory.AbstractComputerMenu;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
@@ -40,14 +39,14 @@ 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
 | 
			
		||||
        );
 | 
			
		||||
        ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
 | 
			
		||||
        graphics.flush(); // Flush to ensure background textures are drawn before foreground.
 | 
			
		||||
        SpriteRenderer.inGui(graphics, spriteRenderer -> {
 | 
			
		||||
            var computerTextures = GuiSprites.getComputerTextures(family);
 | 
			
		||||
            ComputerBorderRenderer.render(
 | 
			
		||||
                spriteRenderer, computerTextures,
 | 
			
		||||
                terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
 | 
			
		||||
            );
 | 
			
		||||
            ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ package dan200.computercraft.client.gui;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
@@ -23,7 +24,7 @@ public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
 | 
			
		||||
        graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
 | 
			
		||||
        graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ 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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,11 @@
 | 
			
		||||
package dan200.computercraft.client.gui;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.gui.Font;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.components.toasts.Toast;
 | 
			
		||||
import net.minecraft.client.gui.components.toasts.ToastComponent;
 | 
			
		||||
import net.minecraft.client.gui.components.toasts.ToastManager;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.FormattedCharSequence;
 | 
			
		||||
@@ -35,8 +37,9 @@ public class ItemToast implements Toast {
 | 
			
		||||
    private final Object token;
 | 
			
		||||
    private final int width;
 | 
			
		||||
 | 
			
		||||
    private boolean isNew = true;
 | 
			
		||||
    private long firstDisplay;
 | 
			
		||||
    private boolean changed = true;
 | 
			
		||||
    private long lastChanged;
 | 
			
		||||
    private Visibility visibility = Visibility.HIDE;
 | 
			
		||||
 | 
			
		||||
    public ItemToast(Minecraft minecraft, ItemStack stack, Component title, Component message, Object token) {
 | 
			
		||||
        this.stack = stack;
 | 
			
		||||
@@ -48,10 +51,10 @@ public class ItemToast implements Toast {
 | 
			
		||||
        width = Math.max(MAX_LINE_SIZE, this.message.stream().mapToInt(font::width).max().orElse(MAX_LINE_SIZE)) + MARGIN * 3 + IMAGE_SIZE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void showOrReplace(ToastComponent toasts) {
 | 
			
		||||
    public void showOrReplace(ToastManager toasts) {
 | 
			
		||||
        var existing = toasts.getToast(ItemToast.class, getToken());
 | 
			
		||||
        if (existing != null) {
 | 
			
		||||
            existing.isNew = true;
 | 
			
		||||
            existing.changed = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            toasts.addToast(this);
 | 
			
		||||
        }
 | 
			
		||||
@@ -73,28 +76,22 @@ public class ItemToast implements Toast {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Visibility render(GuiGraphics graphics, ToastComponent component, long time) {
 | 
			
		||||
        if (isNew) {
 | 
			
		||||
    public Visibility getWantedVisibility() {
 | 
			
		||||
        return visibility;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            firstDisplay = time;
 | 
			
		||||
            isNew = false;
 | 
			
		||||
    @Override
 | 
			
		||||
    public void update(ToastManager toastManager, long time) {
 | 
			
		||||
        if (changed) {
 | 
			
		||||
            lastChanged = time;
 | 
			
		||||
            changed = false;
 | 
			
		||||
        }
 | 
			
		||||
        visibility = time - lastChanged < DISPLAY_TIME * toastManager.getNotificationDisplayTimeMultiplier() ? Visibility.SHOW : Visibility.HIDE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (width == 160 && message.size() <= 1) {
 | 
			
		||||
            graphics.blitSprite(TEXTURE, 0, 0, width, height());
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
            var height = height();
 | 
			
		||||
 | 
			
		||||
            var bottom = Math.min(4, height - 28);
 | 
			
		||||
            renderBackgroundRow(graphics, width, 0, 0, 28);
 | 
			
		||||
 | 
			
		||||
            for (var i = 28; i < height - bottom; i += 10) {
 | 
			
		||||
                renderBackgroundRow(graphics, width, 16, i, Math.min(16, height - i - bottom));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            renderBackgroundRow(graphics, width, 32 - bottom, height - bottom, bottom);
 | 
			
		||||
        }
 | 
			
		||||
    @Override
 | 
			
		||||
    public void render(GuiGraphics graphics, Font font, long time) {
 | 
			
		||||
        graphics.blitSprite(RenderType::guiTextured, TEXTURE, 0, 0, width(), height());
 | 
			
		||||
 | 
			
		||||
        var textX = MARGIN;
 | 
			
		||||
        if (!stack.isEmpty()) {
 | 
			
		||||
@@ -102,23 +99,9 @@ public class ItemToast implements Toast {
 | 
			
		||||
            graphics.renderFakeItem(stack, MARGIN, MARGIN + height() / 2 - IMAGE_SIZE);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        graphics.drawString(component.getMinecraft().font, title, textX, MARGIN, 0xff500050, false);
 | 
			
		||||
        graphics.drawString(font, title, textX, MARGIN, 0xff500050, false);
 | 
			
		||||
        for (var i = 0; i < message.size(); ++i) {
 | 
			
		||||
            graphics.drawString(component.getMinecraft().font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
 | 
			
		||||
            graphics.drawString(font, message.get(i), textX, LINE_SPACING + (i + 1) * LINE_SPACING, 0xff000000, false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return time - firstDisplay < DISPLAY_TIME ? Visibility.SHOW : Visibility.HIDE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void renderBackgroundRow(GuiGraphics graphics, int x, int u, int y, int height) {
 | 
			
		||||
        var leftOffset = u == 0 ? 20 : 5;
 | 
			
		||||
        var rightOffset = Math.min(60, x - leftOffset);
 | 
			
		||||
 | 
			
		||||
        graphics.blitSprite(TEXTURE, 160, 32, 0, u, 0, y, leftOffset, height);
 | 
			
		||||
        for (var k = leftOffset; k < x - rightOffset; k += 64) {
 | 
			
		||||
            graphics.blitSprite(TEXTURE, 160, 32, 32, u, k, y, Math.min(64, x - k - rightOffset), height);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        graphics.blitSprite(TEXTURE, 160, 32, 160 - rightOffset, u, x - rightOffset, y, rightOffset, height);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,14 +10,15 @@ import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
 | 
			
		||||
import net.minecraft.client.KeyMapping;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.ScrollWheelHandler;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.MenuAccess;
 | 
			
		||||
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;
 | 
			
		||||
@@ -32,6 +33,8 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
 | 
			
		||||
    private final Terminal terminalData;
 | 
			
		||||
    private @Nullable TerminalWidget terminal;
 | 
			
		||||
 | 
			
		||||
    private final ScrollWheelHandler scrollHandler = new ScrollWheelHandler();
 | 
			
		||||
 | 
			
		||||
    public NoTermComputerScreen(T menu, Inventory player, Component title) {
 | 
			
		||||
        super(title);
 | 
			
		||||
        this.menu = menu;
 | 
			
		||||
@@ -67,7 +70,12 @@ public class NoTermComputerScreen<T extends AbstractComputerMenu> extends Screen
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
 | 
			
		||||
        Objects.requireNonNull(minecraft().player).getInventory().swapPaint(scrollY);
 | 
			
		||||
        var direction = scrollHandler.onMouseScroll(scrollX, scrollY);
 | 
			
		||||
        var inventory = Objects.requireNonNull(minecraft().player).getInventory();
 | 
			
		||||
        inventory.setSelectedHotbarSlot(ScrollWheelHandler.getNextScrollWheelSelection(
 | 
			
		||||
            direction.y == 0 ? -direction.x : direction.y, inventory.selected, Inventory.getSelectionSize()
 | 
			
		||||
        ));
 | 
			
		||||
 | 
			
		||||
        return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,11 @@ import net.minecraft.client.gui.components.AbstractWidget;
 | 
			
		||||
import net.minecraft.client.gui.components.Button;
 | 
			
		||||
import net.minecraft.client.gui.components.MultiLineLabel;
 | 
			
		||||
import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.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;
 | 
			
		||||
@@ -87,12 +88,13 @@ public final class OptionScreen extends Screen {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
 | 
			
		||||
        // Render the actual texture.
 | 
			
		||||
        graphics.blit(BACKGROUND, x, y, 0, 0, innerWidth, PADDING);
 | 
			
		||||
        graphics.blit(BACKGROUND,
 | 
			
		||||
        graphics.blit(RenderType::guiTextured, BACKGROUND, x, y, 0, 0, innerWidth, PADDING, 256, 256);
 | 
			
		||||
        graphics.blit(RenderType::guiTextured, BACKGROUND,
 | 
			
		||||
            x, y + PADDING, 0, PADDING, innerWidth, innerHeight - PADDING * 2,
 | 
			
		||||
            innerWidth, PADDING
 | 
			
		||||
            innerWidth, PADDING,
 | 
			
		||||
            256, 256
 | 
			
		||||
        );
 | 
			
		||||
        graphics.blit(BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING);
 | 
			
		||||
        graphics.blit(RenderType::guiTextured, BACKGROUND, x, y + innerHeight - PADDING, 0, 256 - PADDING, innerWidth, PADDING, 256, 256);
 | 
			
		||||
 | 
			
		||||
        assertNonNull(messageRenderer).renderLeftAlignedNoShadow(graphics, x + PADDING, y + PADDING, FONT_HEIGHT, 0x404040);
 | 
			
		||||
        super.render(graphics, mouseX, mouseY, partialTicks);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ package dan200.computercraft.client.gui;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.printer.PrinterMenu;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
@@ -23,9 +24,11 @@ public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
 | 
			
		||||
        graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight);
 | 
			
		||||
        graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
 | 
			
		||||
 | 
			
		||||
        if (getMenu().isPrinting()) graphics.blit(BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45);
 | 
			
		||||
        if (getMenu().isPrinting()) {
 | 
			
		||||
            graphics.blit(RenderType::guiTextured, BACKGROUND, leftPos + 34, topPos + 21, 176, 0, 25, 45, 256, 256);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import dan200.computercraft.shared.media.PrintoutMenu;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutData;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 | 
			
		||||
import net.minecraft.client.renderer.LightTexture;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
import net.minecraft.world.inventory.AbstractContainerMenu;
 | 
			
		||||
@@ -20,7 +21,6 @@ import org.lwjgl.glfw.GLFW;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
 | 
			
		||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The GUI for printed pages and books.
 | 
			
		||||
@@ -116,8 +116,10 @@ public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu>
 | 
			
		||||
        graphics.pose().pushPose();
 | 
			
		||||
        graphics.pose().translate(0, 0, 1);
 | 
			
		||||
 | 
			
		||||
        drawBorder(graphics.pose(), graphics.bufferSource(), leftPos, topPos, 0, page, printout.pages(), printout.book(), FULL_BRIGHT_LIGHTMAP);
 | 
			
		||||
        drawText(graphics.pose(), graphics.bufferSource(), leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, printout.text(), printout.colour());
 | 
			
		||||
        graphics.drawSpecial(bufferSource -> {
 | 
			
		||||
            drawBorder(graphics.pose(), bufferSource, leftPos, topPos, 0, page, printout.pages(), printout.book(), LightTexture.FULL_BRIGHT);
 | 
			
		||||
            drawText(graphics.pose(), bufferSource, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, LightTexture.FULL_BRIGHT, printout.text(), printout.colour());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        graphics.pose().popPose();
 | 
			
		||||
    }
 | 
			
		||||
@@ -127,6 +129,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,12 +7,12 @@ package dan200.computercraft.client.gui;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
 | 
			
		||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.SpriteRenderer;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
 | 
			
		||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
@@ -49,8 +49,11 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) {
 | 
			
		||||
        var advanced = family == ComputerFamily.ADVANCED;
 | 
			
		||||
        var texture = advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL;
 | 
			
		||||
        graphics.blit(texture, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0, 0, TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE);
 | 
			
		||||
        graphics.blit(
 | 
			
		||||
            RenderType::guiTextured, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL,
 | 
			
		||||
            leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0,
 | 
			
		||||
            TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Render selected slot
 | 
			
		||||
        var slot = getMenu().getSelectedSlot();
 | 
			
		||||
@@ -58,14 +61,14 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
 | 
			
		||||
            var slotX = slot % 4;
 | 
			
		||||
            var slotY = slot / 4;
 | 
			
		||||
            graphics.blitSprite(
 | 
			
		||||
                advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
 | 
			
		||||
                leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22
 | 
			
		||||
                RenderType::guiTextured, advanced ? SELECTED_ADVANCED : SELECTED_NORMAL,
 | 
			
		||||
                leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 22, 22
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Render sidebar
 | 
			
		||||
        var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES);
 | 
			
		||||
        ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset);
 | 
			
		||||
        graphics.flush(); // Flush to ensure background textures are drawn before foreground.
 | 
			
		||||
        SpriteRenderer.inGui(graphics, spriteRenderer ->
 | 
			
		||||
            ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,16 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui.widgets;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.systems.RenderSystem;
 | 
			
		||||
import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction;
 | 
			
		||||
import net.minecraft.ChatFormatting;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.components.Button;
 | 
			
		||||
import net.minecraft.client.gui.components.Tooltip;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -48,10 +48,7 @@ public class DynamicImageButton extends Button {
 | 
			
		||||
        setTooltip(message.tooltip());
 | 
			
		||||
 | 
			
		||||
        var texture = this.texture.get(isHoveredOrFocused());
 | 
			
		||||
 | 
			
		||||
        RenderSystem.disableDepthTest();
 | 
			
		||||
        graphics.blitSprite(texture, getX(), getY(), 0, width, height);
 | 
			
		||||
        RenderSystem.enableDepthTest();
 | 
			
		||||
        graphics.blitSprite(RenderType::guiTextured, texture, getX(), getY(), width, height);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record HintedMessage(Component message, Tooltip tooltip) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui.widgets;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.core.util.StringUtil;
 | 
			
		||||
@@ -70,7 +69,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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -254,12 +253,12 @@ public class TerminalWidget extends AbstractWidget {
 | 
			
		||||
    public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
 | 
			
		||||
        if (!visible) return;
 | 
			
		||||
 | 
			
		||||
        var emitter = FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), graphics.bufferSource().getBuffer(RenderTypes.TERMINAL));
 | 
			
		||||
 | 
			
		||||
        FixedWidthFontRenderer.drawTerminal(
 | 
			
		||||
            emitter,
 | 
			
		||||
            (float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
 | 
			
		||||
        );
 | 
			
		||||
        graphics.drawSpecial(bufferSource -> {
 | 
			
		||||
            FixedWidthFontRenderer.drawTerminal(
 | 
			
		||||
                FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
 | 
			
		||||
                (float) innerX, (float) innerY, terminal, (float) MARGIN, (float) MARGIN, (float) MARGIN, (float) MARGIN
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -5,17 +5,18 @@
 | 
			
		||||
package dan200.computercraft.client.integration;
 | 
			
		||||
 | 
			
		||||
import com.google.auto.service.AutoService;
 | 
			
		||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.shared.platform.PlatformHelper;
 | 
			
		||||
import net.irisshaders.iris.api.v0.IrisApi;
 | 
			
		||||
import net.irisshaders.iris.api.v0.IrisTextVertexSink;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.client.renderer.LightTexture;
 | 
			
		||||
import org.lwjgl.system.MemoryUtil;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.function.IntFunction;
 | 
			
		||||
 | 
			
		||||
@AutoService(ShaderMod.Provider.class)
 | 
			
		||||
public class IrisShaderMod implements ShaderMod.Provider {
 | 
			
		||||
@@ -31,7 +32,7 @@ public class IrisShaderMod implements ShaderMod.Provider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
 | 
			
		||||
        public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, ByteBufferBuilder makeBuffer) {
 | 
			
		||||
            return IrisApi.getInstance().getMinorApiRevision() >= 1
 | 
			
		||||
                ? new IrisQuadEmitter(vertexCount, makeBuffer)
 | 
			
		||||
                : super.getQuadEmitter(vertexCount, makeBuffer);
 | 
			
		||||
@@ -39,25 +40,24 @@ public class IrisShaderMod implements ShaderMod.Provider {
 | 
			
		||||
 | 
			
		||||
        private static final class IrisQuadEmitter implements DirectFixedWidthFontRenderer.QuadEmitter {
 | 
			
		||||
            private final IrisTextVertexSink sink;
 | 
			
		||||
            private @Nullable ByteBuffer buffer;
 | 
			
		||||
 | 
			
		||||
            private IrisQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
 | 
			
		||||
                sink = IrisApi.getInstance().createTextVertexSink(vertexCount, makeBuffer);
 | 
			
		||||
            private IrisQuadEmitter(int vertexCount, ByteBufferBuilder builder) {
 | 
			
		||||
                sink = IrisApi.getInstance().createTextVertexSink(vertexCount, i -> {
 | 
			
		||||
                    if (buffer != null) throw new IllegalStateException("Allocated multiple buffers");
 | 
			
		||||
                    return buffer = MemoryUtil.memByteBuffer(builder.reserve(i), i);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
 | 
			
		||||
                sink.quad(x1, y1, x2, y2, z, colour, u1, v1, u2, v2, LightTexture.FULL_BRIGHT);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public VertexFormat format() {
 | 
			
		||||
                return sink.getUnderlyingVertexFormat();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public ByteBuffer buffer() {
 | 
			
		||||
                return sink.getUnderlyingByteBuffer();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
 | 
			
		||||
                sink.quad(x1, y1, x2, y2, z, FastColor.ABGR32.fromArgb32(colour), u1, v1, u2, v2, RenderTypes.FULL_BRIGHT_LIGHTMAP);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,12 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.integration;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexBuffer;
 | 
			
		||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.ServiceLoader;
 | 
			
		||||
import java.util.function.IntFunction;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Find the currently loaded shader mod (if present) and provides utilities for interacting with it.
 | 
			
		||||
@@ -31,16 +29,14 @@ public class ShaderMod {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an appropriate quad emitter for use with {@link DirectVertexBuffer} and {@link DirectFixedWidthFontRenderer} .
 | 
			
		||||
     * Get an appropriate quad emitter for use with {@link VertexBuffer} and {@link DirectFixedWidthFontRenderer} .
 | 
			
		||||
     *
 | 
			
		||||
     * @param vertexCount The number of vertices.
 | 
			
		||||
     * @param makeBuffer  A function to allocate a temporary buffer.
 | 
			
		||||
     * @param buffer      A function to allocate a temporary buffer.
 | 
			
		||||
     * @return The quad emitter.
 | 
			
		||||
     */
 | 
			
		||||
    public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, IntFunction<ByteBuffer> makeBuffer) {
 | 
			
		||||
        return new DirectFixedWidthFontRenderer.ByteBufferEmitter(
 | 
			
		||||
            makeBuffer.apply(RenderTypes.TERMINAL.format().getVertexSize() * vertexCount * 4)
 | 
			
		||||
        );
 | 
			
		||||
    public DirectFixedWidthFontRenderer.QuadEmitter getQuadEmitter(int vertexCount, ByteBufferBuilder buffer) {
 | 
			
		||||
        return new DirectFixedWidthFontRenderer.ByteBufferEmitter(buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface Provider {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,43 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.item.colour;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
 | 
			
		||||
import dan200.computercraft.client.pocket.PocketComputerData;
 | 
			
		||||
import net.minecraft.client.color.item.ItemTintSource;
 | 
			
		||||
import net.minecraft.client.multiplayer.ClientLevel;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import net.minecraft.util.ExtraCodecs;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An {@link ItemTintSource} that returns the pocket computer's {@linkplain PocketComputerData#getLightState() light
 | 
			
		||||
 * colour}.
 | 
			
		||||
 *
 | 
			
		||||
 * @param defaultColour The default colour, if the light is not currently on.
 | 
			
		||||
 */
 | 
			
		||||
public record PocketComputerLight(int defaultColour) implements ItemTintSource {
 | 
			
		||||
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_light");
 | 
			
		||||
    public static final MapCodec<PocketComputerLight> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
 | 
			
		||||
        ExtraCodecs.RGB_COLOR_CODEC.fieldOf("default").forGetter(PocketComputerLight::defaultColour)
 | 
			
		||||
    ).apply(instance, PocketComputerLight::new));
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int calculate(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder) {
 | 
			
		||||
        var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
        return computer == null || computer.getLightState() == -1 ? defaultColour : ARGB.opaque(computer.getLightState());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MapCodec<? extends ItemTintSource> type() {
 | 
			
		||||
        return CODEC;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.item.model;
 | 
			
		||||
 | 
			
		||||
import net.minecraft.client.renderer.Sheets;
 | 
			
		||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.DelegateBakedModel;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link BakedModel} that wraps another model, but providing different {@link ItemTransforms}.
 | 
			
		||||
 */
 | 
			
		||||
class BakedModelWithTransform extends DelegateBakedModel {
 | 
			
		||||
    private final ItemTransforms transforms;
 | 
			
		||||
 | 
			
		||||
    BakedModelWithTransform(BakedModel bakedModel, ItemTransforms transforms) {
 | 
			
		||||
        super(bakedModel);
 | 
			
		||||
        this.transforms = transforms;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static void addLayer(ItemStackRenderState state, BakedModel model, ItemTransforms transforms) {
 | 
			
		||||
        state.newLayer().setupBlockModel(new BakedModelWithTransform(model, transforms), Sheets.translucentItemSheet());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ItemTransforms getTransforms() {
 | 
			
		||||
        return transforms;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.item.model;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.multiplayer.ClientLevel;
 | 
			
		||||
import net.minecraft.client.renderer.block.model.ItemTransforms;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemModel;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemModelResolver;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import net.minecraft.world.item.ItemDisplayContext;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An {@link ItemModel} that renders the {@linkplain TurtleOverlay turtle overlay}.
 | 
			
		||||
 *
 | 
			
		||||
 * @param transforms The item transformations from the base model.
 | 
			
		||||
 * @see TurtleOverlay#model()
 | 
			
		||||
 */
 | 
			
		||||
public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel {
 | 
			
		||||
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/overlay");
 | 
			
		||||
    public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
 | 
			
		||||
        ResourceLocation.CODEC.fieldOf("transforms").forGetter(Unbaked::base)
 | 
			
		||||
    ).apply(instance, Unbaked::new));
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int light) {
 | 
			
		||||
        var overlay = TurtleItem.getOverlay(stack);
 | 
			
		||||
        if (overlay == null) return;
 | 
			
		||||
 | 
			
		||||
        var model = ClientPlatformHelper.get().getModel(Minecraft.getInstance().getModelManager(), overlay.model());
 | 
			
		||||
        BakedModelWithTransform.addLayer(state, model, transforms());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record Unbaked(ResourceLocation base) implements ItemModel.Unbaked {
 | 
			
		||||
        @Override
 | 
			
		||||
        public MapCodec<Unbaked> type() {
 | 
			
		||||
            return CODEC;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ItemModel bake(BakingContext bakingContext) {
 | 
			
		||||
            return new TurtleOverlayModel(bakingContext.bake(base).getTransforms());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void resolveDependencies(Resolver resolver) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,94 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.item.model;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.math.Transformation;
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import net.minecraft.client.multiplayer.ClientLevel;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemModel;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemModelResolver;
 | 
			
		||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
 | 
			
		||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import net.minecraft.world.item.ItemDisplayContext;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An {@link ItemModel} that renders a turtle upgrade, using its {@link TurtleUpgradeModeller}.
 | 
			
		||||
 *
 | 
			
		||||
 * @param side The side the upgrade resides on.
 | 
			
		||||
 * @param base The base model. Only used to provide item transforms.
 | 
			
		||||
 */
 | 
			
		||||
public record TurtleUpgradeModel(TurtleSide side, BakedModel base) implements ItemModel {
 | 
			
		||||
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/upgrade");
 | 
			
		||||
    public static final MapCodec<Unbaked> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
 | 
			
		||||
        TurtleSide.CODEC.fieldOf("side").forGetter(Unbaked::side),
 | 
			
		||||
        ResourceLocation.CODEC.fieldOf("transforms").forGetter(Unbaked::base)
 | 
			
		||||
    ).apply(instance, Unbaked::new));
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void update(ItemStackRenderState state, ItemStack stack, ItemModelResolver resolver, ItemDisplayContext context, @Nullable ClientLevel level, @Nullable LivingEntity holder, int light) {
 | 
			
		||||
        var upgrade = TurtleItem.getUpgradeWithData(stack, side);
 | 
			
		||||
        if (upgrade == null) return;
 | 
			
		||||
 | 
			
		||||
        switch (TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side)) {
 | 
			
		||||
            case TransformedModel.Item model -> {
 | 
			
		||||
                var childState = new ItemStackRenderState();
 | 
			
		||||
                resolver.updateForTopItem(childState, model.stack(), ItemDisplayContext.NONE, false, level, null, 0);
 | 
			
		||||
                if (!childState.isEmpty()) {
 | 
			
		||||
                    state.newLayer().setupSpecialModel(new TransformedRenderer(childState, model.transformation()), null, base);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            case TransformedModel.Baked baked ->
 | 
			
		||||
                BakedModelWithTransform.addLayer(state, baked.model(), base.getTransforms());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record Unbaked(TurtleSide side, ResourceLocation base) implements ItemModel.Unbaked {
 | 
			
		||||
        @Override
 | 
			
		||||
        public MapCodec<Unbaked> type() {
 | 
			
		||||
            return CODEC;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ItemModel bake(BakingContext bakingContext) {
 | 
			
		||||
            return new TurtleUpgradeModel(side, bakingContext.bake(base));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void resolveDependencies(Resolver resolver) {
 | 
			
		||||
            resolver.resolve(base);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record TransformedRenderer(
 | 
			
		||||
        ItemStackRenderState state, Transformation transform
 | 
			
		||||
    ) implements SpecialModelRenderer<Void> {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void render(@Nullable Void object, ItemDisplayContext itemDisplayContext, PoseStack poseStack, MultiBufferSource multiBufferSource, int overlay, int light, boolean bl) {
 | 
			
		||||
            poseStack.pushPose();
 | 
			
		||||
            poseStack.mulPose(transform.getMatrix());
 | 
			
		||||
            state.render(poseStack, multiBufferSource, overlay, light);
 | 
			
		||||
            poseStack.popPose();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public @Nullable Void extractArgument(ItemStack itemStack) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.item.properties;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerState;
 | 
			
		||||
import net.minecraft.client.multiplayer.ClientLevel;
 | 
			
		||||
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import net.minecraft.world.item.ItemDisplayContext;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link SelectItemModelProperty} that returns the pocket computer's current state.
 | 
			
		||||
 */
 | 
			
		||||
public final class PocketComputerStateProperty implements SelectItemModelProperty<ComputerState> {
 | 
			
		||||
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_state");
 | 
			
		||||
    private static final PocketComputerStateProperty INSTANCE = new PocketComputerStateProperty();
 | 
			
		||||
    public static final MapCodec<PocketComputerStateProperty> CODEC = MapCodec.unit(INSTANCE);
 | 
			
		||||
    public static final Type<PocketComputerStateProperty, ComputerState> TYPE = Type.create(CODEC, ComputerState.CODEC);
 | 
			
		||||
 | 
			
		||||
    private PocketComputerStateProperty() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static PocketComputerStateProperty create() {
 | 
			
		||||
        return INSTANCE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ComputerState get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
 | 
			
		||||
        var computer = ClientPocketComputers.get(stack);
 | 
			
		||||
        return computer == null ? ComputerState.OFF : computer.getState();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Type<? extends SelectItemModelProperty<ComputerState>, ComputerState> type() {
 | 
			
		||||
        return TYPE;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.item.properties;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import net.minecraft.client.multiplayer.ClientLevel;
 | 
			
		||||
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.LivingEntity;
 | 
			
		||||
import net.minecraft.world.item.ItemDisplayContext;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An item property that determines whether the turtle's current {@linkplain TurtleOverlay overlay} is compatible
 | 
			
		||||
 * with the Christmas overlay.
 | 
			
		||||
 *
 | 
			
		||||
 * @see TurtleOverlay#showElfOverlay()
 | 
			
		||||
 */
 | 
			
		||||
public class TurtleShowElfOverlay implements ConditionalItemModelProperty {
 | 
			
		||||
    public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle/show_elf_overlay");
 | 
			
		||||
    private static final TurtleShowElfOverlay INSTANCE = new TurtleShowElfOverlay();
 | 
			
		||||
    public static final MapCodec<TurtleShowElfOverlay> CODEC = MapCodec.unit(INSTANCE);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
 | 
			
		||||
        var overlay = TurtleItem.getOverlay(stack);
 | 
			
		||||
        return overlay == null || overlay.showElfOverlay();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TurtleShowElfOverlay create() {
 | 
			
		||||
        return INSTANCE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public MapCodec<? extends ConditionalItemModelProperty> type() {
 | 
			
		||||
        return CODEC;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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.renderer.texture.TextureAtlas;
 | 
			
		||||
import net.minecraft.client.resources.model.Material;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
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(TextureAtlas.LOCATION_BLOCKS, TEXTURE_NORMAL);
 | 
			
		||||
    private static final Material MATERIAL_ADVANCED = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_ADVANCED);
 | 
			
		||||
    private static final Material MATERIAL_COLOUR = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_COLOUR);
 | 
			
		||||
    private static final Material MATERIAL_FRAME = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_FRAME);
 | 
			
		||||
    private static final Material MATERIAL_LIGHT = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE_LIGHT);
 | 
			
		||||
 | 
			
		||||
    // The size of the terminal within the model.
 | 
			
		||||
    public static final float TERM_WIDTH = 12.0f / 32.0f;
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,9 +13,9 @@ import net.minecraft.client.model.geom.ModelPart;
 | 
			
		||||
import net.minecraft.client.model.geom.PartPose;
 | 
			
		||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
 | 
			
		||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
 | 
			
		||||
import net.minecraft.client.renderer.texture.TextureAtlas;
 | 
			
		||||
import net.minecraft.client.resources.model.Material;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.inventory.InventoryMenu;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +29,7 @@ import java.util.List;
 | 
			
		||||
 */
 | 
			
		||||
public class LecternPrintoutModel {
 | 
			
		||||
    public static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/printout");
 | 
			
		||||
    public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE);
 | 
			
		||||
    public static final Material MATERIAL = new Material(TextureAtlas.LOCATION_BLOCKS, TEXTURE);
 | 
			
		||||
 | 
			
		||||
    private static final int TEXTURE_WIDTH = 32;
 | 
			
		||||
    private static final int TEXTURE_HEIGHT = 32;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,92 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.model.turtle;
 | 
			
		||||
 | 
			
		||||
import com.mojang.math.Transformation;
 | 
			
		||||
import net.minecraft.client.renderer.block.model.BakedQuad;
 | 
			
		||||
import net.minecraft.client.renderer.block.model.FaceBakery;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
import org.joml.Vector4f;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Applies a {@link Transformation} (or rather a {@link Matrix4f}) to a list of {@link BakedQuad}s.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This does a little bit of magic compared with other system (i.e. Forge's {@code QuadTransformers}), as it needs to
 | 
			
		||||
 * handle flipping models upside down.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
 | 
			
		||||
 */
 | 
			
		||||
public class ModelTransformer {
 | 
			
		||||
    private static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
 | 
			
		||||
 | 
			
		||||
    private static final int STRIDE = FaceBakery.VERTEX_INT_SIZE;
 | 
			
		||||
    private static final int POS_OFFSET = 0;
 | 
			
		||||
 | 
			
		||||
    protected final Matrix4f transformation;
 | 
			
		||||
    protected final boolean invert;
 | 
			
		||||
    private @Nullable TransformedQuads cache;
 | 
			
		||||
 | 
			
		||||
    public ModelTransformer(Transformation transformation) {
 | 
			
		||||
        this.transformation = transformation.getMatrix();
 | 
			
		||||
        invert = transformation.getMatrix().determinant() < 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public List<BakedQuad> transform(List<BakedQuad> quads) {
 | 
			
		||||
        if (quads.isEmpty()) return List.of();
 | 
			
		||||
 | 
			
		||||
        // We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
 | 
			
		||||
        // so it's not worth being smarter here.
 | 
			
		||||
        var cache = this.cache;
 | 
			
		||||
        if (cache != null && quads.equals(cache.original())) return cache.transformed();
 | 
			
		||||
 | 
			
		||||
        List<BakedQuad> transformed = new ArrayList<>(quads.size());
 | 
			
		||||
        for (var quad : quads) transformed.add(transformQuad(quad));
 | 
			
		||||
        this.cache = new TransformedQuads(quads, transformed);
 | 
			
		||||
        return transformed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private BakedQuad transformQuad(BakedQuad quad) {
 | 
			
		||||
        var inputData = quad.getVertices();
 | 
			
		||||
        var outputData = new int[inputData.length];
 | 
			
		||||
        for (var i = 0; i < 4; i++) {
 | 
			
		||||
            var inStart = STRIDE * i;
 | 
			
		||||
            // Reverse the order of the quads if we're inverting
 | 
			
		||||
            var outStart = getVertexOffset(i, invert);
 | 
			
		||||
            System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
 | 
			
		||||
 | 
			
		||||
            // Apply the matrix to our position
 | 
			
		||||
            var inPosStart = inStart + POS_OFFSET;
 | 
			
		||||
            var outPosStart = outStart + POS_OFFSET;
 | 
			
		||||
 | 
			
		||||
            var x = Float.intBitsToFloat(inputData[inPosStart]);
 | 
			
		||||
            var y = Float.intBitsToFloat(inputData[inPosStart + 1]);
 | 
			
		||||
            var z = Float.intBitsToFloat(inputData[inPosStart + 2]);
 | 
			
		||||
 | 
			
		||||
            // Transform the position
 | 
			
		||||
            var pos = new Vector4f(x, y, z, 1);
 | 
			
		||||
            transformation.transformProject(pos);
 | 
			
		||||
 | 
			
		||||
            outputData[outPosStart] = Float.floatToRawIntBits(pos.x());
 | 
			
		||||
            outputData[outPosStart + 1] = Float.floatToRawIntBits(pos.y());
 | 
			
		||||
            outputData[outPosStart + 2] = Float.floatToRawIntBits(pos.z());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var direction = Direction.rotate(transformation, quad.getDirection());
 | 
			
		||||
        return new BakedQuad(outputData, quad.getTintIndex(), direction, quad.getSprite(), quad.isShade());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static int getVertexOffset(int vertex, boolean invert) {
 | 
			
		||||
        return (invert ? ModelTransformer.INVERSE_ORDER[vertex] : vertex) * ModelTransformer.STRIDE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,146 +0,0 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.model.turtle;
 | 
			
		||||
 | 
			
		||||
import com.google.common.cache.CacheBuilder;
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.math.Transformation;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeData;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import dan200.computercraft.shared.util.DataComponentUtil;
 | 
			
		||||
import dan200.computercraft.shared.util.Holiday;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelManager;
 | 
			
		||||
import net.minecraft.core.component.DataComponents;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Combines several individual models together to form a turtle.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of the resulting "baked model".
 | 
			
		||||
 */
 | 
			
		||||
public final class TurtleModelParts<T> {
 | 
			
		||||
    private static final Transformation identity, flip;
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        var stack = new PoseStack();
 | 
			
		||||
        stack.translate(0.5f, 0.5f, 0.5f);
 | 
			
		||||
        stack.scale(1, -1, 1);
 | 
			
		||||
        stack.translate(-0.5f, -0.5f, -0.5f);
 | 
			
		||||
 | 
			
		||||
        identity = Transformation.identity();
 | 
			
		||||
        flip = new Transformation(stack.last().pose());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record Combination(
 | 
			
		||||
        boolean colour,
 | 
			
		||||
        @Nullable UpgradeData<ITurtleUpgrade> leftUpgrade,
 | 
			
		||||
        @Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
 | 
			
		||||
        @Nullable TurtleOverlay overlay,
 | 
			
		||||
        boolean christmas,
 | 
			
		||||
        boolean flip
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final BakedModel familyModel;
 | 
			
		||||
    private final BakedModel colourModel;
 | 
			
		||||
    private final Function<TransformedModel, BakedModel> transformer;
 | 
			
		||||
    private final Function<Combination, T> buildModel;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed
 | 
			
		||||
     * instances, reducing memory usage and hopefully ensuring their caches are hit more often!
 | 
			
		||||
     */
 | 
			
		||||
    private final Map<TransformedModel, BakedModel> transformCache = CacheBuilder.newBuilder()
 | 
			
		||||
        .concurrencyLevel(1)
 | 
			
		||||
        .expireAfterAccess(30, TimeUnit.SECONDS)
 | 
			
		||||
        .<TransformedModel, BakedModel>build()
 | 
			
		||||
        .asMap();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A cache of {@link Combination}s to the combined model.
 | 
			
		||||
     */
 | 
			
		||||
    private final Map<Combination, T> modelCache = CacheBuilder.newBuilder()
 | 
			
		||||
        .concurrencyLevel(1)
 | 
			
		||||
        .expireAfterAccess(30, TimeUnit.SECONDS)
 | 
			
		||||
        .<Combination, T>build()
 | 
			
		||||
        .asMap();
 | 
			
		||||
 | 
			
		||||
    public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function<List<BakedModel>, T> combineModel) {
 | 
			
		||||
        this.familyModel = familyModel;
 | 
			
		||||
        this.colourModel = colourModel;
 | 
			
		||||
        this.transformer = x -> transformer.transform(x.model(), x.matrix());
 | 
			
		||||
        buildModel = x -> combineModel.apply(buildModel(x));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public T getModel(ItemStack stack) {
 | 
			
		||||
        var combination = getCombination(stack);
 | 
			
		||||
        return modelCache.computeIfAbsent(combination, buildModel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Combination getCombination(ItemStack stack) {
 | 
			
		||||
        var christmas = Holiday.getCurrent() == Holiday.CHRISTMAS;
 | 
			
		||||
        var leftUpgrade = TurtleItem.getUpgradeWithData(stack, TurtleSide.LEFT);
 | 
			
		||||
        var rightUpgrade = TurtleItem.getUpgradeWithData(stack, TurtleSide.RIGHT);
 | 
			
		||||
        var overlay = TurtleItem.getOverlay(stack);
 | 
			
		||||
        var label = DataComponentUtil.getCustomName(stack);
 | 
			
		||||
        var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm"));
 | 
			
		||||
 | 
			
		||||
        return new Combination(stack.has(DataComponents.DYED_COLOR), leftUpgrade, rightUpgrade, overlay, christmas, flip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<BakedModel> buildModel(Combination combo) {
 | 
			
		||||
        var mc = Minecraft.getInstance();
 | 
			
		||||
        var modelManager = mc.getItemRenderer().getItemModelShaper().getModelManager();
 | 
			
		||||
 | 
			
		||||
        var transformation = combo.flip ? flip : identity;
 | 
			
		||||
        var parts = new ArrayList<BakedModel>(4);
 | 
			
		||||
        parts.add(transform(combo.colour() ? colourModel : familyModel, transformation));
 | 
			
		||||
 | 
			
		||||
        if (combo.overlay() != null) addPart(parts, modelManager, transformation, combo.overlay().model());
 | 
			
		||||
 | 
			
		||||
        var showChristmas = TurtleOverlay.showElfOverlay(combo.overlay(), combo.christmas());
 | 
			
		||||
        if (showChristmas) addPart(parts, modelManager, transformation, TurtleOverlay.ELF_MODEL);
 | 
			
		||||
 | 
			
		||||
        addUpgrade(parts, transformation, TurtleSide.LEFT, combo.leftUpgrade());
 | 
			
		||||
        addUpgrade(parts, transformation, TurtleSide.RIGHT, combo.rightUpgrade());
 | 
			
		||||
 | 
			
		||||
        return parts;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addPart(List<BakedModel> parts, ModelManager modelManager, Transformation transformation, ResourceLocation model) {
 | 
			
		||||
        parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, model), transformation));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addUpgrade(List<BakedModel> parts, Transformation transformation, TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
 | 
			
		||||
        if (upgrade == null) return;
 | 
			
		||||
        var model = TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side);
 | 
			
		||||
        parts.add(transform(model.model(), transformation.compose(model.matrix())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private BakedModel transform(BakedModel model, Transformation transformation) {
 | 
			
		||||
        if (transformation.equals(Transformation.identity())) return model;
 | 
			
		||||
        return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface ModelTransformer {
 | 
			
		||||
        BakedModel transform(BakedModel model, Transformation transformation);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,13 +22,15 @@ import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.core.Holder;
 | 
			
		||||
import net.minecraft.core.registries.Registries;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.JukeboxSong;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
import net.minecraft.world.level.block.LevelEvent;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -62,10 +64,14 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handlePlayRecord(BlockPos pos, @Nullable Holder<JukeboxSong> song) {
 | 
			
		||||
        var level = Minecraft.getInstance().level;
 | 
			
		||||
        if (level == null) return;
 | 
			
		||||
 | 
			
		||||
        if (song == null) {
 | 
			
		||||
            Minecraft.getInstance().levelRenderer.stopJukeboxSongAndNotifyNearby(pos);
 | 
			
		||||
            level.levelEvent(LevelEvent.SOUND_STOP_JUKEBOX_SONG, pos, 0);
 | 
			
		||||
        } else {
 | 
			
		||||
            Minecraft.getInstance().levelRenderer.playJukeboxSong(song, pos);
 | 
			
		||||
            var id = level.registryAccess().lookupOrThrow(Registries.JUKEBOX_SONG).getIdOrThrow(song.value());
 | 
			
		||||
            level.levelEvent(LevelEvent.SOUND_PLAY_JUKEBOX_SONG, pos, id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import dan200.computercraft.client.ClientHooks;
 | 
			
		||||
import net.minecraft.client.Camera;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.client.renderer.LevelRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import net.minecraft.util.CommonColors;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Utilities for rendering block outline.
 | 
			
		||||
 *
 | 
			
		||||
 * @see ClientHooks#drawHighlight(PoseStack, MultiBufferSource, Camera, BlockHitResult)
 | 
			
		||||
 */
 | 
			
		||||
public final class BlockOutlineRenderer {
 | 
			
		||||
    private BlockOutlineRenderer() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render a block outline, handling both normal and high-contrast modes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param bufferSource The buffer source.
 | 
			
		||||
     * @param renderer     The function to render a highlight.
 | 
			
		||||
     * @see LevelRenderer#renderBlockOutline(Camera, MultiBufferSource.BufferSource, PoseStack, boolean)
 | 
			
		||||
     */
 | 
			
		||||
    public static void render(MultiBufferSource bufferSource, Renderer renderer) {
 | 
			
		||||
        var highContrast = Minecraft.getInstance().options.highContrastBlockOutline().get();
 | 
			
		||||
        if (highContrast) renderer.render(bufferSource.getBuffer(RenderType.secondaryBlockOutline()), 0xff000000);
 | 
			
		||||
 | 
			
		||||
        var colour = highContrast ? CommonColors.HIGH_CONTRAST_DIAMOND : ARGB.color(0x66, CommonColors.BLACK);
 | 
			
		||||
        renderer.render(bufferSource.getBuffer(RenderType.lines()), colour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @FunctionalInterface
 | 
			
		||||
    public interface Renderer {
 | 
			
		||||
        void render(VertexConsumer buffer, int colour);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,8 +11,7 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
 | 
			
		||||
import dan200.computercraft.shared.util.WorldUtil;
 | 
			
		||||
import net.minecraft.client.Camera;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.util.Mth;
 | 
			
		||||
import net.minecraft.client.renderer.ShapeRenderer;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
 | 
			
		||||
public final class CableHighlightRenderer {
 | 
			
		||||
@@ -27,7 +26,6 @@ public final class CableHighlightRenderer {
 | 
			
		||||
     * @param camera       The current camera.
 | 
			
		||||
     * @param hit          The block hit result for the current player.
 | 
			
		||||
     * @return If we rendered a custom outline.
 | 
			
		||||
     * @see net.minecraft.client.renderer.LevelRenderer#renderHitOutline
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean drawHighlight(PoseStack transform, MultiBufferSource bufferSource, Camera camera, BlockHitResult hit) {
 | 
			
		||||
        var pos = hit.getBlockPos();
 | 
			
		||||
@@ -49,27 +47,9 @@ public final class CableHighlightRenderer {
 | 
			
		||||
        var yOffset = pos.getY() - cameraPos.y();
 | 
			
		||||
        var zOffset = pos.getZ() - cameraPos.z();
 | 
			
		||||
 | 
			
		||||
        var buffer = bufferSource.getBuffer(RenderType.lines());
 | 
			
		||||
        var matrix4f = transform.last().pose();
 | 
			
		||||
        // TODO: Can we just accesstransformer out LevelRenderer.renderShape?
 | 
			
		||||
        shape.forAllEdges((x1, y1, z1, x2, y2, z2) -> {
 | 
			
		||||
            var xDelta = (float) (x2 - x1);
 | 
			
		||||
            var yDelta = (float) (y2 - y1);
 | 
			
		||||
            var zDelta = (float) (z2 - z1);
 | 
			
		||||
            var len = Mth.sqrt(xDelta * xDelta + yDelta * yDelta + zDelta * zDelta);
 | 
			
		||||
            xDelta = xDelta / len;
 | 
			
		||||
            yDelta = yDelta / len;
 | 
			
		||||
            zDelta = zDelta / len;
 | 
			
		||||
 | 
			
		||||
            buffer
 | 
			
		||||
                .addVertex(matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset))
 | 
			
		||||
                .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
                .setNormal(transform.last(), xDelta, yDelta, zDelta);
 | 
			
		||||
            buffer
 | 
			
		||||
                .addVertex(matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset))
 | 
			
		||||
                .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
                .setNormal(transform.last(), xDelta, yDelta, zDelta);
 | 
			
		||||
        });
 | 
			
		||||
        BlockOutlineRenderer.render(
 | 
			
		||||
            bufferSource, (buffer, colour) -> ShapeRenderer.renderShape(transform, buffer, shape, xOffset, yOffset, zOffset, colour)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,16 +6,30 @@ 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.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.ARGB;
 | 
			
		||||
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 +37,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
 | 
			
		||||
@@ -45,8 +66,46 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
 | 
			
		||||
            } 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),
 | 
			
		||||
                ARGB.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
 | 
			
		||||
            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(FixedWidthFontRenderer.TERMINAL_TEXT));
 | 
			
		||||
            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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutData;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutItem;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
 | 
			
		||||
import net.minecraft.world.entity.decoration.ItemFrame;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Additional render state attached to a {@link ItemFrameRenderState}.
 | 
			
		||||
 *
 | 
			
		||||
 * @see dan200.computercraft.client.ClientHooks#onRenderItemFrame(PoseStack, MultiBufferSource, ItemFrameRenderState, ExtendedItemFrameRenderState, int)
 | 
			
		||||
 */
 | 
			
		||||
public class ExtendedItemFrameRenderState {
 | 
			
		||||
    public @Nullable PrintoutData printoutData;
 | 
			
		||||
    public boolean isBook;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set up the render state from the {@link ItemFrame}'s {@link ItemStack}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The item frame's item.
 | 
			
		||||
     */
 | 
			
		||||
    public void setup(ItemStack stack) {
 | 
			
		||||
        if (stack.getItem() instanceof PrintoutItem) {
 | 
			
		||||
            printoutData = PrintoutData.getOrEmpty(stack);
 | 
			
		||||
            isBook = stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get();
 | 
			
		||||
        } else {
 | 
			
		||||
            printoutData = null;
 | 
			
		||||
            isBook = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,16 +6,14 @@ package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import dan200.computercraft.client.model.turtle.ModelTransformer;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.block.model.BakedQuad;
 | 
			
		||||
import net.minecraft.client.renderer.entity.ItemRenderer;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import org.joml.Vector4f;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -29,8 +27,7 @@ public final class ModelRenderer {
 | 
			
		||||
     * Render a list of {@linkplain BakedQuad quads} to a buffer.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is not intended to be used directly, but instead by {@link ClientPlatformHelper#renderBakedModel(PoseStack, MultiBufferSource, BakedModel, int, int, int[])}. The
 | 
			
		||||
     * implementation here is pretty similar to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, ItemStack, int, int)},
 | 
			
		||||
     * but supports inverted quads (i.e. those with a negative scale).
 | 
			
		||||
     * implementation here is identical to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, int[], int, int)}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param transform     The current matrix transformation to apply.
 | 
			
		||||
     * @param buffer        The buffer to draw to.
 | 
			
		||||
@@ -39,60 +36,23 @@ 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;
 | 
			
		||||
 | 
			
		||||
        for (var bakedquad : quads) {
 | 
			
		||||
            var tint = -1;
 | 
			
		||||
            float r = 1.0f, g = 1.0f, b = 1.0f, a = 1.0f;
 | 
			
		||||
            if (tints != null && bakedquad.isTinted()) {
 | 
			
		||||
                var idx = bakedquad.getTintIndex();
 | 
			
		||||
                if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
 | 
			
		||||
                if (idx >= 0 && idx < tints.length) {
 | 
			
		||||
                    var tint = tints[bakedquad.getTintIndex()];
 | 
			
		||||
                    r = ARGB.red(tint) / 255.0f;
 | 
			
		||||
                    g = ARGB.green(tint) / 255.0f;
 | 
			
		||||
                    b = ARGB.blue(tint) / 255.0f;
 | 
			
		||||
                    a = ARGB.alpha(tint) / 255.0f;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            putBulkQuad(buffer, matrix, bakedquad, tint, lightmapCoord, overlayLight, inverted);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, float, int, int)} which
 | 
			
		||||
     * will reverse vertex order when the matrix is inverted.
 | 
			
		||||
     *
 | 
			
		||||
     * @param buffer        The buffer to draw to.
 | 
			
		||||
     * @param pose          The current matrix stack.
 | 
			
		||||
     * @param quad          The quad to draw.
 | 
			
		||||
     * @param colour        The tint for this quad.
 | 
			
		||||
     * @param lightmapCoord The lightmap coordinate
 | 
			
		||||
     * @param overlayLight  The overlay light.
 | 
			
		||||
     * @param invert        Whether to reverse the order of this quad.
 | 
			
		||||
     */
 | 
			
		||||
    private static void putBulkQuad(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, int colour, int lightmapCoord, int overlayLight, boolean invert) {
 | 
			
		||||
        var matrix = pose.pose();
 | 
			
		||||
        // It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
 | 
			
		||||
        // Direction.rotate (so not out of nowhere!), but is a little suspicious.
 | 
			
		||||
        var dirNormal = quad.getDirection().getNormal();
 | 
			
		||||
        var vector = new Vector4f();
 | 
			
		||||
 | 
			
		||||
        matrix.transform(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f, vector).normalize();
 | 
			
		||||
        float normalX = vector.x(), normalY = vector.y(), normalZ = vector.z();
 | 
			
		||||
 | 
			
		||||
        var vertices = quad.getVertices();
 | 
			
		||||
        for (var vertex = 0; vertex < 4; vertex++) {
 | 
			
		||||
            var i = ModelTransformer.getVertexOffset(vertex, invert);
 | 
			
		||||
 | 
			
		||||
            var x = Float.intBitsToFloat(vertices[i]);
 | 
			
		||||
            var y = Float.intBitsToFloat(vertices[i + 1]);
 | 
			
		||||
            var z = Float.intBitsToFloat(vertices[i + 2]);
 | 
			
		||||
 | 
			
		||||
            matrix.transform(x, y, z, 1, vector);
 | 
			
		||||
 | 
			
		||||
            var u = Float.intBitsToFloat(vertices[i + 4]);
 | 
			
		||||
            var v = Float.intBitsToFloat(vertices[i + 5]);
 | 
			
		||||
            buffer.addVertex(
 | 
			
		||||
                vector.x(), vector.y(), vector.z(),
 | 
			
		||||
                colour, u, v, overlayLight, lightmapCoord,
 | 
			
		||||
                normalX, normalY, normalZ
 | 
			
		||||
            );
 | 
			
		||||
            buffer.putBulkData(matrix, bakedquad, r, g, b, a, lightmapCoord, overlayLight);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,10 @@ 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.renderer.LightTexture;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.component.DyedItemColor;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
@@ -72,7 +74,7 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
        var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
 | 
			
		||||
        renderLight(transform, bufferSource, lightColour, width, height);
 | 
			
		||||
 | 
			
		||||
        var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
 | 
			
		||||
        var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT));
 | 
			
		||||
        if (terminal == null) {
 | 
			
		||||
            FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -89,16 +91,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
        var g = (colour >>> 8) & 0xFF;
 | 
			
		||||
        var b = colour & 0xFF;
 | 
			
		||||
 | 
			
		||||
        var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b);
 | 
			
		||||
        var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderType.text(GuiSprites.TEXTURE)), 0, light, r, g, b);
 | 
			
		||||
        ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
 | 
			
		||||
        var buffer = render.getBuffer(RenderTypes.TERMINAL);
 | 
			
		||||
        var buffer = render.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
 | 
			
		||||
        FixedWidthFontRenderer.drawQuad(
 | 
			
		||||
            FixedWidthFontRenderer.toVertexConsumer(transform, buffer),
 | 
			
		||||
            width - LIGHT_HEIGHT * 2, height + BORDER / 2.0f, 0.001f, LIGHT_HEIGHT * 2, LIGHT_HEIGHT,
 | 
			
		||||
            FastColor.ARGB32.opaque(colour), RenderTypes.FULL_BRIGHT_LIGHTMAP
 | 
			
		||||
            ARGB.opaque(colour), LightTexture.FULL_BRIGHT
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,8 @@ import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutData;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutItem;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.world.entity.EntityType;
 | 
			
		||||
import net.minecraft.world.entity.decoration.ItemFrame;
 | 
			
		||||
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
 | 
			
		||||
@@ -35,25 +33,22 @@ public final class PrintoutItemRenderer extends ItemMapLikeRenderer {
 | 
			
		||||
        transform.scale(0.42f, 0.42f, -0.42f);
 | 
			
		||||
        transform.translate(-0.5f, -0.48f, 0.0f);
 | 
			
		||||
 | 
			
		||||
        drawPrintout(transform, render, stack, light);
 | 
			
		||||
        drawPrintout(transform, render, PrintoutData.getOrEmpty(stack), stack.getItem() == ModRegistry.Items.PRINTED_BOOK.get(), light);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrame frame, ItemStack stack, int packedLight) {
 | 
			
		||||
    public static void onRenderInFrame(PoseStack transform, MultiBufferSource render, ItemFrameRenderState frame, PrintoutData data, boolean isBook, int packedLight) {
 | 
			
		||||
        // Move a little bit forward to ensure we're not clipping with the frame
 | 
			
		||||
        transform.translate(0.0f, 0.0f, -0.001f);
 | 
			
		||||
        transform.mulPose(Axis.ZP.rotationDegrees(180f));
 | 
			
		||||
        transform.scale(0.95f, 0.95f, -0.95f);
 | 
			
		||||
        transform.translate(-0.5f, -0.5f, 0.0f);
 | 
			
		||||
 | 
			
		||||
        var light = frame.getType() == EntityType.GLOW_ITEM_FRAME ? 0xf000d2 : packedLight; // See getLightVal.
 | 
			
		||||
        drawPrintout(transform, render, stack, light);
 | 
			
		||||
        var light = frame.isGlowFrame ? 0xf000d2 : packedLight; // See getLightCoords.
 | 
			
		||||
        drawPrintout(transform, render, data, isBook, light);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void drawPrintout(PoseStack transform, MultiBufferSource render, ItemStack stack, int light) {
 | 
			
		||||
        var pageData = stack.getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
 | 
			
		||||
 | 
			
		||||
    private static void drawPrintout(PoseStack transform, MultiBufferSource render, PrintoutData pageData, boolean book, int light) {
 | 
			
		||||
        var pages = pageData.pages();
 | 
			
		||||
        var book = ((PrintoutItem) stack.getItem()).getType() == PrintoutItem.Type.BOOK;
 | 
			
		||||
 | 
			
		||||
        double width = LINE_LENGTH * FONT_WIDTH + X_TEXT_MARGIN * 2;
 | 
			
		||||
        double height = LINES_PER_PAGE * FONT_HEIGHT + Y_TEXT_MARGIN * 2;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ import dan200.computercraft.core.terminal.Palette;
 | 
			
		||||
import dan200.computercraft.core.terminal.TextBuffer;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutData;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@@ -23,6 +25,12 @@ import static dan200.computercraft.shared.media.items.PrintoutData.LINES_PER_PAG
 | 
			
		||||
 * {@linkplain PrintoutItemRenderer in-hand/item frame printouts}.
 | 
			
		||||
 */
 | 
			
		||||
public final class PrintoutRenderer {
 | 
			
		||||
    /**
 | 
			
		||||
     * Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
 | 
			
		||||
     * it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
 | 
			
		||||
     */
 | 
			
		||||
    private static final RenderType BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
 | 
			
		||||
 | 
			
		||||
    private static final float BG_SIZE = 256.0f;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -58,11 +66,14 @@ public final class PrintoutRenderer {
 | 
			
		||||
    private static final int COVER_Y = Y_SIZE;
 | 
			
		||||
    private static final int COVER_X = X_SIZE + 4 * X_FOLD_SIZE;
 | 
			
		||||
 | 
			
		||||
    private static final float BOOK_Z_OFFSET = -0.1f;
 | 
			
		||||
    private static final float PAGE_Z_OFFSET = -0.01f;
 | 
			
		||||
 | 
			
		||||
    private PrintoutRenderer() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, TextBuffer[] text, TextBuffer[] colours) {
 | 
			
		||||
        var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
 | 
			
		||||
        var buffer = bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
 | 
			
		||||
        var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
 | 
			
		||||
        for (var line = 0; line < LINES_PER_PAGE && line < text.length; line++) {
 | 
			
		||||
            FixedWidthFontRenderer.drawString(emitter,
 | 
			
		||||
@@ -73,7 +84,7 @@ public final class PrintoutRenderer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void drawText(PoseStack transform, MultiBufferSource bufferSource, int x, int y, int start, int light, List<PrintoutData.Line> lines) {
 | 
			
		||||
        var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_TEXT);
 | 
			
		||||
        var buffer = bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT);
 | 
			
		||||
        var emitter = FixedWidthFontRenderer.toVertexConsumer(transform, buffer);
 | 
			
		||||
        for (var line = 0; line < LINES_PER_PAGE && line < lines.size(); line++) {
 | 
			
		||||
            var lineContents = lines.get(start + line);
 | 
			
		||||
@@ -90,7 +101,7 @@ public final class PrintoutRenderer {
 | 
			
		||||
        var leftPages = page;
 | 
			
		||||
        var rightPages = pages - page - 1;
 | 
			
		||||
 | 
			
		||||
        var buffer = bufferSource.getBuffer(RenderTypes.PRINTOUT_BACKGROUND);
 | 
			
		||||
        var buffer = bufferSource.getBuffer(BACKGROUND);
 | 
			
		||||
 | 
			
		||||
        if (isBook) {
 | 
			
		||||
            // Border
 | 
			
		||||
@@ -99,12 +110,12 @@ public final class PrintoutRenderer {
 | 
			
		||||
            var right = x + X_SIZE + offset - 4;
 | 
			
		||||
 | 
			
		||||
            // Left and right border
 | 
			
		||||
            drawTexture(matrix, buffer, left - 4, y - 8, z - 0.02f, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
 | 
			
		||||
            drawTexture(matrix, buffer, right, y - 8, z - 0.02f, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
 | 
			
		||||
            drawTexture(matrix, buffer, left - 4, y - 8, z + BOOK_Z_OFFSET, COVER_X, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
 | 
			
		||||
            drawTexture(matrix, buffer, right, y - 8, z + BOOK_Z_OFFSET, COVER_X + COVER_SIZE, 0, COVER_SIZE, Y_SIZE + COVER_SIZE * 2, light);
 | 
			
		||||
 | 
			
		||||
            // Draw centre panel (just stretched texture, sorry).
 | 
			
		||||
            drawTexture(matrix, buffer,
 | 
			
		||||
                x - offset, y, z - 0.02f, X_SIZE + offset * 2, Y_SIZE,
 | 
			
		||||
                x - offset, y, z + BOOK_Z_OFFSET, X_SIZE + offset * 2, Y_SIZE,
 | 
			
		||||
                COVER_X + COVER_SIZE / 2.0f, COVER_SIZE, COVER_SIZE, Y_SIZE,
 | 
			
		||||
                light
 | 
			
		||||
            );
 | 
			
		||||
@@ -112,20 +123,20 @@ public final class PrintoutRenderer {
 | 
			
		||||
            var borderX = left;
 | 
			
		||||
            while (borderX < right) {
 | 
			
		||||
                double thisWidth = Math.min(right - borderX, X_SIZE);
 | 
			
		||||
                drawTexture(matrix, buffer, borderX, y - 8, z - 0.02f, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
 | 
			
		||||
                drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z - 0.02f, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
 | 
			
		||||
                drawTexture(matrix, buffer, borderX, y - 8, z + BOOK_Z_OFFSET, 0, COVER_Y, (float) thisWidth, COVER_SIZE, light);
 | 
			
		||||
                drawTexture(matrix, buffer, borderX, y + Y_SIZE - 4, z + BOOK_Z_OFFSET, 0, COVER_Y + COVER_SIZE, (float) thisWidth, COVER_SIZE, light);
 | 
			
		||||
                borderX = (float) (borderX + thisWidth);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Current page background: Z-offset is interleaved between the "zeroth" left/right page and the first
 | 
			
		||||
        // left/right page, so that the "bold" border can be drawn over the edge where appropriate.
 | 
			
		||||
        drawTexture(matrix, buffer, x, y, z - 1e-3f * 0.5f, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
 | 
			
		||||
        drawTexture(matrix, buffer, x, y, z + PAGE_Z_OFFSET, X_FOLD_SIZE * 2, 0, X_SIZE, Y_SIZE, light);
 | 
			
		||||
 | 
			
		||||
        // Left pages
 | 
			
		||||
        for (var n = 0; n <= leftPages; n++) {
 | 
			
		||||
            drawTexture(matrix, buffer,
 | 
			
		||||
                x - offsetAt(n), y, z - 1e-3f * n,
 | 
			
		||||
                x - offsetAt(n), y, z + PAGE_Z_OFFSET * (n + 1),
 | 
			
		||||
                // Use the left "bold" fold for the outermost page
 | 
			
		||||
                n == leftPages ? 0 : X_FOLD_SIZE, 0,
 | 
			
		||||
                X_FOLD_SIZE, Y_SIZE, light
 | 
			
		||||
@@ -135,7 +146,7 @@ public final class PrintoutRenderer {
 | 
			
		||||
        // Right pages
 | 
			
		||||
        for (var n = 0; n <= rightPages; n++) {
 | 
			
		||||
            drawTexture(matrix, buffer,
 | 
			
		||||
                x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z - 1e-3f * n,
 | 
			
		||||
                x + (X_SIZE - X_FOLD_SIZE) + offsetAt(n), y, z + PAGE_Z_OFFSET * (n + 1),
 | 
			
		||||
                // Two folds, then the main page. Use the right "bold" fold for the outermost page.
 | 
			
		||||
                X_FOLD_SIZE * 2 + X_SIZE + (n == rightPages ? X_FOLD_SIZE : 0), 0,
 | 
			
		||||
                X_FOLD_SIZE, Y_SIZE, light
 | 
			
		||||
 
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.client.gui.GuiSprites;
 | 
			
		||||
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.GameRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.RenderStateShard;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.client.renderer.ShaderInstance;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceProvider;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Shared {@link RenderType}s used throughout the mod.
 | 
			
		||||
 */
 | 
			
		||||
public class RenderTypes {
 | 
			
		||||
    public static final int FULL_BRIGHT_LIGHTMAP = (0xF << 4) | (0xF << 20);
 | 
			
		||||
 | 
			
		||||
    private static @Nullable MonitorTextureBufferShader monitorTboShader;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Renders a fullbright terminal.
 | 
			
		||||
     */
 | 
			
		||||
    public static final RenderType TERMINAL = RenderType.text(FixedWidthFontRenderer.FONT);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Renders a monitor with the TBO shader.
 | 
			
		||||
     *
 | 
			
		||||
     * @see MonitorTextureBufferShader
 | 
			
		||||
     */
 | 
			
		||||
    public static final RenderType MONITOR_TBO = Types.MONITOR_TBO;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A variant of {@link #TERMINAL} which uses the lightmap rather than rendering fullbright.
 | 
			
		||||
     */
 | 
			
		||||
    public static final RenderType PRINTOUT_TEXT = RenderType.text(FixedWidthFontRenderer.FONT);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Printout's background texture. {@link RenderType#text(ResourceLocation)} is a <em>little</em> questionable, but
 | 
			
		||||
     * it is what maps use, so should behave the same as vanilla in both item frames and in-hand.
 | 
			
		||||
     */
 | 
			
		||||
    public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render type for {@linkplain GuiSprites GUI sprites}.
 | 
			
		||||
     */
 | 
			
		||||
    public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE);
 | 
			
		||||
 | 
			
		||||
    public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
 | 
			
		||||
        if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
 | 
			
		||||
        return monitorTboShader;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ShaderInstance getTerminalShader() {
 | 
			
		||||
        return Objects.requireNonNull(GameRenderer.getRendertypeTextShader(), "Text shader has not been registered");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void registerShaders(ResourceProvider resources, BiConsumer<ShaderInstance, Consumer<ShaderInstance>> load) throws IOException {
 | 
			
		||||
        load.accept(
 | 
			
		||||
            new MonitorTextureBufferShader(
 | 
			
		||||
                resources,
 | 
			
		||||
                ComputerCraftAPI.MOD_ID + "/monitor_tbo",
 | 
			
		||||
                MONITOR_TBO.format()
 | 
			
		||||
            ),
 | 
			
		||||
            x -> monitorTboShader = (MonitorTextureBufferShader) x
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final class Types extends RenderType {
 | 
			
		||||
        private static final RenderStateShard.TextureStateShard TERM_FONT_TEXTURE = new TextureStateShard(
 | 
			
		||||
            FixedWidthFontRenderer.FONT,
 | 
			
		||||
            false, false // blur, minimap
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        static final RenderType MONITOR_TBO = RenderType.create(
 | 
			
		||||
            "monitor_tbo", DefaultVertexFormat.POSITION_TEX, VertexFormat.Mode.TRIANGLE_STRIP, 128,
 | 
			
		||||
            false, false, // useDelegate, needsSorting
 | 
			
		||||
            RenderType.CompositeState.builder()
 | 
			
		||||
                .setTextureState(TERM_FONT_TEXTURE)
 | 
			
		||||
                .setShaderState(new ShaderStateShard(RenderTypes::getMonitorTextureBufferShader))
 | 
			
		||||
                .createCompositeState(false)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        @SuppressWarnings("UnusedMethod")
 | 
			
		||||
        private Types(String name, VertexFormat format, VertexFormat.Mode mode, int buffer, boolean crumbling, boolean sort, Runnable setup, Runnable teardown) {
 | 
			
		||||
            super(name, format, mode, buffer, crumbling, sort, setup, teardown);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,11 +5,15 @@
 | 
			
		||||
package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import dan200.computercraft.client.gui.GuiSprites;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.renderer.LightTexture;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link GuiGraphics}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity
 | 
			
		||||
 * renderer).
 | 
			
		||||
@@ -34,11 +38,11 @@ public class SpriteRenderer {
 | 
			
		||||
        this.b = b;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 static void inGui(GuiGraphics graphics, Consumer<SpriteRenderer> renderer) {
 | 
			
		||||
        graphics.drawSpecial(bufferSource -> renderer.accept(new SpriteRenderer(
 | 
			
		||||
            graphics.pose().last().pose(), bufferSource.getBuffer(RenderType.guiTextured(GuiSprites.TEXTURE)),
 | 
			
		||||
            0, LightTexture.FULL_BRIGHT, 255, 255, 255
 | 
			
		||||
        )));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -68,7 +72,7 @@ public class SpriteRenderer {
 | 
			
		||||
     * @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.
 | 
			
		||||
        // TODO(1.21.4): Drive this from mcmeta files, like vanilla does.
 | 
			
		||||
        if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders");
 | 
			
		||||
 | 
			
		||||
        var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth);
 | 
			
		||||
@@ -93,7 +97,7 @@ public class SpriteRenderer {
 | 
			
		||||
     * @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.
 | 
			
		||||
        // TODO(1.21.4): Drive this from mcmeta files, like vanilla does.
 | 
			
		||||
        if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders");
 | 
			
		||||
 | 
			
		||||
        var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight);
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,12 @@ package dan200.computercraft.client.render;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import com.mojang.math.Transformation;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.util.Holiday;
 | 
			
		||||
@@ -22,14 +23,17 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
 | 
			
		||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import net.minecraft.util.CommonColors;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.util.Mth;
 | 
			
		||||
import net.minecraft.world.item.ItemDisplayContext;
 | 
			
		||||
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 NORMAL_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_normal");
 | 
			
		||||
    public static final ResourceLocation ADVANCED_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_advanced");
 | 
			
		||||
    public static final ResourceLocation COLOUR_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
 | 
			
		||||
 | 
			
		||||
    private final BlockEntityRenderDispatcher renderer;
 | 
			
		||||
@@ -73,9 +77,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        transform.translate(0.5f, 0.5f, 0.5f);
 | 
			
		||||
        var yaw = turtle.getRenderYaw(partialTicks);
 | 
			
		||||
        transform.mulPose(Axis.YP.rotationDegrees(180.0f - yaw));
 | 
			
		||||
        if (label != null && (label.equals("Dinnerbone") || label.equals("Grumm"))) {
 | 
			
		||||
            transform.scale(1.0f, -1.0f, 1.0f);
 | 
			
		||||
        }
 | 
			
		||||
        transform.translate(-0.5f, -0.5f, -0.5f);
 | 
			
		||||
 | 
			
		||||
        // Render the turtle
 | 
			
		||||
@@ -83,14 +84,10 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        var overlay = turtle.getOverlay();
 | 
			
		||||
 | 
			
		||||
        if (colour == -1) {
 | 
			
		||||
            // Render the turtle using its item model.
 | 
			
		||||
            var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper();
 | 
			
		||||
            var model = modelManager.getItemModel(turtle.getBlockState().getBlock().asItem());
 | 
			
		||||
            if (model == null) model = modelManager.getModelManager().getMissingModel();
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, model, null);
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Otherwise render it using the colour item.
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ FastColor.ARGB32.opaque(colour) });
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ ARGB.opaque(colour) });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Render the overlay
 | 
			
		||||
@@ -117,15 +114,25 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
 | 
			
		||||
        transform.translate(0.0f, -0.5f, -0.5f);
 | 
			
		||||
 | 
			
		||||
        var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
 | 
			
		||||
        applyTransformation(transform, model.matrix());
 | 
			
		||||
        renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
 | 
			
		||||
        switch (TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side)) {
 | 
			
		||||
            case TransformedModel.Item model -> {
 | 
			
		||||
                transform.mulPose(model.transformation().getMatrix());
 | 
			
		||||
                transform.mulPose(Axis.YP.rotation(Mth.PI));
 | 
			
		||||
                Minecraft.getInstance().getItemRenderer().renderStatic(
 | 
			
		||||
                    model.stack(), ItemDisplayContext.FIXED, lightmapCoord, overlayLight, transform, buffers, turtle.getLevel(), 0
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            case TransformedModel.Baked model ->
 | 
			
		||||
                renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        transform.popPose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, @Nullable int[] tints) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getModelManager();
 | 
			
		||||
    private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, int @Nullable [] tints) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -140,19 +147,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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void applyTransformation(PoseStack stack, Transformation transformation) {
 | 
			
		||||
        var trans = transformation.getTranslation();
 | 
			
		||||
        stack.translate(trans.x(), trans.y(), trans.z());
 | 
			
		||||
 | 
			
		||||
        stack.mulPose(transformation.getLeftRotation());
 | 
			
		||||
 | 
			
		||||
        var scale = transformation.getScale();
 | 
			
		||||
        stack.scale(scale.x(), scale.y(), scale.z());
 | 
			
		||||
 | 
			
		||||
        stack.mulPose(transformation.getRightRotation());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,40 +4,28 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render.monitor;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.platform.GlStateManager;
 | 
			
		||||
import com.mojang.blaze3d.systems.RenderSystem;
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.blaze3d.vertex.Tesselator;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexBuffer;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import com.mojang.blaze3d.vertex.*;
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import dan200.computercraft.annotations.ForgeOverride;
 | 
			
		||||
import dan200.computercraft.client.FrameInfo;
 | 
			
		||||
import dan200.computercraft.client.integration.ShaderMod;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.client.render.vbo.DirectBuffers;
 | 
			
		||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.core.util.Nullability;
 | 
			
		||||
import dan200.computercraft.shared.config.Config;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
 | 
			
		||||
import dan200.computercraft.shared.util.DirectionUtil;
 | 
			
		||||
import net.minecraft.client.renderer.CompiledShaderProgram;
 | 
			
		||||
import net.minecraft.client.renderer.FogParameters;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
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.lwjgl.opengl.GL11;
 | 
			
		||||
import org.lwjgl.opengl.GL20;
 | 
			
		||||
import org.lwjgl.opengl.GL31;
 | 
			
		||||
import org.lwjgl.system.MemoryUtil;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
 | 
			
		||||
@@ -51,9 +39,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
     */
 | 
			
		||||
    private static final float MARGIN = (float) (MonitorBlockEntity.RENDER_MARGIN * 1.1);
 | 
			
		||||
 | 
			
		||||
    private static @Nullable ByteBuffer backingBuffer;
 | 
			
		||||
 | 
			
		||||
    private static long lastFrame = -1;
 | 
			
		||||
    private static final ByteBufferBuilder backingBufferBuilder = new ByteBufferBuilder(0x4000);
 | 
			
		||||
 | 
			
		||||
    public MonitorBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
 | 
			
		||||
    }
 | 
			
		||||
@@ -76,7 +62,6 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        lastFrame = renderFrame;
 | 
			
		||||
        renderState.lastRenderFrame = renderFrame;
 | 
			
		||||
        renderState.lastRenderPos = monitorPos;
 | 
			
		||||
 | 
			
		||||
@@ -124,7 +109,7 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
            transform.popPose();
 | 
			
		||||
        } else {
 | 
			
		||||
            FixedWidthFontRenderer.drawEmptyTerminal(
 | 
			
		||||
                FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
 | 
			
		||||
                FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(FixedWidthFontRenderer.TERMINAL_TEXT)),
 | 
			
		||||
                -MARGIN, MARGIN,
 | 
			
		||||
                (float) (xSize + 2 * MARGIN), (float) -(ySize + MARGIN * 2)
 | 
			
		||||
            );
 | 
			
		||||
@@ -136,126 +121,100 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
    private static void renderTerminal(
 | 
			
		||||
        Matrix4f matrix, ClientMonitor monitor, MonitorRenderState renderState, Terminal terminal, float xMargin, float yMargin
 | 
			
		||||
    ) {
 | 
			
		||||
        int width = terminal.getWidth(), height = terminal.getHeight();
 | 
			
		||||
        int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
 | 
			
		||||
 | 
			
		||||
        var renderType = currentRenderer();
 | 
			
		||||
        var redraw = monitor.pollTerminalChanged();
 | 
			
		||||
        if (renderState.createBuffer(renderType)) redraw = true;
 | 
			
		||||
        if (renderState.createBuffer()) redraw = true;
 | 
			
		||||
 | 
			
		||||
        switch (renderType) {
 | 
			
		||||
            case TBO -> {
 | 
			
		||||
                if (redraw) {
 | 
			
		||||
                    var terminalBuffer = getBuffer(width * height * 3);
 | 
			
		||||
                    MonitorTextureBufferShader.setTerminalData(terminalBuffer, terminal);
 | 
			
		||||
                    DirectBuffers.setBufferData(GL31.GL_TEXTURE_BUFFER, renderState.tboBuffer, terminalBuffer, GL20.GL_STATIC_DRAW);
 | 
			
		||||
        var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
 | 
			
		||||
        var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
 | 
			
		||||
        if (redraw) {
 | 
			
		||||
            var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
 | 
			
		||||
 | 
			
		||||
                    var uniformBuffer = getBuffer(MonitorTextureBufferShader.UNIFORM_SIZE);
 | 
			
		||||
                    MonitorTextureBufferShader.setUniformData(uniformBuffer, terminal);
 | 
			
		||||
                    DirectBuffers.setBufferData(GL31.GL_UNIFORM_BUFFER, renderState.tboUniform, uniformBuffer, GL20.GL_STATIC_DRAW);
 | 
			
		||||
                }
 | 
			
		||||
            // In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
 | 
			
		||||
            // a starting and ending offset, and so need to use two buffers instead.
 | 
			
		||||
 | 
			
		||||
                // Nobody knows what they're doing!
 | 
			
		||||
                var active = GlStateManager._getActiveTexture();
 | 
			
		||||
                RenderSystem.activeTexture(MonitorTextureBufferShader.TEXTURE_INDEX);
 | 
			
		||||
                GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, renderState.tboTexture);
 | 
			
		||||
                RenderSystem.activeTexture(active);
 | 
			
		||||
            renderToBuffer(backgroundBuffer, size, sink ->
 | 
			
		||||
                DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
 | 
			
		||||
 | 
			
		||||
                var shader = RenderTypes.getMonitorTextureBufferShader();
 | 
			
		||||
                shader.setupUniform(renderState.tboUniform);
 | 
			
		||||
 | 
			
		||||
                var buffer = Tesselator.getInstance().begin(RenderTypes.MONITOR_TBO.mode(), RenderTypes.MONITOR_TBO.format());
 | 
			
		||||
                tboVertex(buffer, matrix, -xMargin, -yMargin);
 | 
			
		||||
                tboVertex(buffer, matrix, -xMargin, pixelHeight + yMargin);
 | 
			
		||||
                tboVertex(buffer, matrix, pixelWidth + xMargin, -yMargin);
 | 
			
		||||
                tboVertex(buffer, matrix, pixelWidth + xMargin, pixelHeight + yMargin);
 | 
			
		||||
                RenderTypes.MONITOR_TBO.draw(Nullability.assertNonNull(buffer.build()));
 | 
			
		||||
            }
 | 
			
		||||
            case VBO -> {
 | 
			
		||||
                var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
 | 
			
		||||
                var foregroundBuffer = assertNonNull(renderState.foregroundBuffer);
 | 
			
		||||
                if (redraw) {
 | 
			
		||||
                    var size = DirectFixedWidthFontRenderer.getVertexCount(terminal);
 | 
			
		||||
 | 
			
		||||
                    // In an ideal world we could upload these both into one buffer. However, we can't render VBOs with
 | 
			
		||||
                    // a starting and ending offset, and so need to use two buffers instead.
 | 
			
		||||
 | 
			
		||||
                    renderToBuffer(backgroundBuffer, size, sink ->
 | 
			
		||||
                        DirectFixedWidthFontRenderer.drawTerminalBackground(sink, 0, 0, terminal, yMargin, yMargin, xMargin, xMargin));
 | 
			
		||||
 | 
			
		||||
                    renderToBuffer(foregroundBuffer, size, sink -> {
 | 
			
		||||
                        DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
 | 
			
		||||
                        // If the cursor is visible, we append it to the end of our buffer. When rendering, we can either
 | 
			
		||||
                        // render n or n+1 quads and so toggle the cursor on and off.
 | 
			
		||||
                        DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will
 | 
			
		||||
                // use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the
 | 
			
		||||
                // normal render distance (~200), and the edges of the monitor fade out due to fog.
 | 
			
		||||
                // There's not really a good way around this, at least without using a custom render type (which the VBO
 | 
			
		||||
                // renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
 | 
			
		||||
                // absurdly high value.
 | 
			
		||||
                var oldFogStart = RenderSystem.getShaderFogStart();
 | 
			
		||||
                RenderSystem.setShaderFogStart(1e4f);
 | 
			
		||||
 | 
			
		||||
                RenderTypes.TERMINAL.setupRenderState();
 | 
			
		||||
 | 
			
		||||
                // Compose the existing model view matrix with our transformation matrix.
 | 
			
		||||
                var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix);
 | 
			
		||||
 | 
			
		||||
                // Render background geometry
 | 
			
		||||
                backgroundBuffer.bind();
 | 
			
		||||
                backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader());
 | 
			
		||||
 | 
			
		||||
                // Render foreground geometry with glPolygonOffset enabled.
 | 
			
		||||
                RenderSystem.polygonOffset(-1.0f, -10.0f);
 | 
			
		||||
                RenderSystem.enablePolygonOffset();
 | 
			
		||||
 | 
			
		||||
                foregroundBuffer.bind();
 | 
			
		||||
                foregroundBuffer.drawWithShader(
 | 
			
		||||
                    modelView, RenderSystem.getProjectionMatrix(), RenderTypes.getTerminalShader(),
 | 
			
		||||
                    // Skip the cursor quad if it is not visible this frame.
 | 
			
		||||
                    FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
 | 
			
		||||
                        ? foregroundBuffer.getIndexCount() - RenderTypes.TERMINAL.mode().indexCount(4)
 | 
			
		||||
                        : foregroundBuffer.getIndexCount()
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Clear state
 | 
			
		||||
                RenderSystem.polygonOffset(0.0f, -0.0f);
 | 
			
		||||
                RenderSystem.disablePolygonOffset();
 | 
			
		||||
                RenderTypes.TERMINAL.clearRenderState();
 | 
			
		||||
                VertexBuffer.unbind();
 | 
			
		||||
 | 
			
		||||
                RenderSystem.setShaderFogStart(oldFogStart);
 | 
			
		||||
            }
 | 
			
		||||
            case BEST -> throw new IllegalStateException("Impossible: Should never use BEST renderer");
 | 
			
		||||
            renderToBuffer(foregroundBuffer, size + 4, sink -> {
 | 
			
		||||
                DirectFixedWidthFontRenderer.drawTerminalForeground(sink, 0, 0, terminal);
 | 
			
		||||
                // If the cursor is visible, we append it to the end of our buffer. When rendering, we can either
 | 
			
		||||
                // render n or n+1 quads and so toggle the cursor on and off.
 | 
			
		||||
                DirectFixedWidthFontRenderer.drawCursor(sink, 0, 0, terminal);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Our VBO renders coordinates in monitor-space rather than world space. A full sized monitor (8x6) will
 | 
			
		||||
        // use positions from (0, 0) to (164*FONT_WIDTH, 81*FONT_HEIGHT) = (984, 729). This is far outside the
 | 
			
		||||
        // normal render distance (~200), and the edges of the monitor fade out due to fog.
 | 
			
		||||
        // There's not really a good way around this, at least without using a custom render type (which the VBO
 | 
			
		||||
        // renderer is trying to avoid!). Instead, we just disable fog entirely by setting the fog start to an
 | 
			
		||||
        // absurdly high value.
 | 
			
		||||
        var oldFog = RenderSystem.getShaderFog();
 | 
			
		||||
        RenderSystem.setShaderFog(FogParameters.NO_FOG);
 | 
			
		||||
 | 
			
		||||
        FixedWidthFontRenderer.TERMINAL_TEXT.setupRenderState();
 | 
			
		||||
 | 
			
		||||
        // Compose the existing model view matrix with our transformation matrix.
 | 
			
		||||
        var modelView = new Matrix4f(RenderSystem.getModelViewMatrix()).mul(matrix);
 | 
			
		||||
 | 
			
		||||
        // Render background geometry
 | 
			
		||||
        backgroundBuffer.bind();
 | 
			
		||||
        backgroundBuffer.drawWithShader(modelView, RenderSystem.getProjectionMatrix(), RenderSystem.getShader());
 | 
			
		||||
 | 
			
		||||
        // Render foreground geometry with glPolygonOffset enabled.
 | 
			
		||||
        RenderSystem.polygonOffset(-1.0f, -10.0f);
 | 
			
		||||
        RenderSystem.enablePolygonOffset();
 | 
			
		||||
 | 
			
		||||
        foregroundBuffer.bind();
 | 
			
		||||
        drawWithShader(
 | 
			
		||||
            foregroundBuffer, modelView, RenderSystem.getProjectionMatrix(), RenderSystem.getShader(),
 | 
			
		||||
            // Skip the cursor quad if it is not visible this frame.
 | 
			
		||||
            FixedWidthFontRenderer.isCursorVisible(terminal) && !FrameInfo.getGlobalCursorBlink()
 | 
			
		||||
                ? foregroundBuffer.indexCount - FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(4)
 | 
			
		||||
                : foregroundBuffer.indexCount
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Clear state
 | 
			
		||||
        RenderSystem.polygonOffset(0.0f, -0.0f);
 | 
			
		||||
        RenderSystem.disablePolygonOffset();
 | 
			
		||||
        FixedWidthFontRenderer.TERMINAL_TEXT.clearRenderState();
 | 
			
		||||
        VertexBuffer.unbind();
 | 
			
		||||
 | 
			
		||||
        RenderSystem.setShaderFog(oldFog);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void renderToBuffer(DirectVertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
 | 
			
		||||
        var sink = ShaderMod.get().getQuadEmitter(size, MonitorBlockEntityRenderer::getBuffer);
 | 
			
		||||
        var buffer = sink.buffer();
 | 
			
		||||
 | 
			
		||||
    private static void renderToBuffer(VertexBuffer vbo, int size, Consumer<DirectFixedWidthFontRenderer.QuadEmitter> draw) {
 | 
			
		||||
        var sink = ShaderMod.get().getQuadEmitter(size, backingBufferBuilder);
 | 
			
		||||
        draw.accept(sink);
 | 
			
		||||
        buffer.flip();
 | 
			
		||||
        vbo.upload(buffer.limit() / sink.format().getVertexSize(), RenderTypes.TERMINAL.mode(), sink.format(), buffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void tboVertex(VertexConsumer builder, Matrix4f matrix, float x, float y) {
 | 
			
		||||
        // We encode position in the UV, as that's not transformed by the matrix.
 | 
			
		||||
        builder.addVertex(matrix, x, y, 0).setUv(x, y);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ByteBuffer getBuffer(int capacity) {
 | 
			
		||||
        var buffer = backingBuffer;
 | 
			
		||||
        if (buffer == null || buffer.capacity() < capacity) {
 | 
			
		||||
            buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
 | 
			
		||||
        var result = backingBufferBuilder.build();
 | 
			
		||||
        if (result == null) {
 | 
			
		||||
            // If we have nothing to draw, just mark it as empty. We'll skip drawing in drawWithShader.
 | 
			
		||||
            vbo.indexCount = 0;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        buffer.clear();
 | 
			
		||||
        return buffer;
 | 
			
		||||
        var buffer = result.byteBuffer();
 | 
			
		||||
        var vertices = buffer.limit() / sink.format().getVertexSize();
 | 
			
		||||
 | 
			
		||||
        vbo.bind();
 | 
			
		||||
        vbo.upload(new MeshData(result, new MeshData.DrawState(
 | 
			
		||||
            sink.format(),
 | 
			
		||||
            vertices, FixedWidthFontRenderer.TERMINAL_TEXT.mode().indexCount(vertices),
 | 
			
		||||
            FixedWidthFontRenderer.TERMINAL_TEXT.mode(), VertexFormat.IndexType.least(vertices)
 | 
			
		||||
        )));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void drawWithShader(VertexBuffer buffer, Matrix4f modelView, Matrix4f projection, @Nullable CompiledShaderProgram compiledShaderProgram, int indicies) {
 | 
			
		||||
        var originalIndexCount = buffer.indexCount;
 | 
			
		||||
        if (originalIndexCount == 0) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            buffer.indexCount = indicies;
 | 
			
		||||
            buffer.drawWithShader(modelView, projection, compiledShaderProgram);
 | 
			
		||||
        } finally {
 | 
			
		||||
            buffer.indexCount = originalIndexCount;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -267,28 +226,4 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
    public AABB getRenderBoundingBox(MonitorBlockEntity monitor) {
 | 
			
		||||
        return monitor.getRenderBoundingBox();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine if any monitors were rendered this frame.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether any monitors were rendered.
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean hasRenderedThisFrame() {
 | 
			
		||||
        return FrameInfo.getRenderFrame() == lastFrame;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the current renderer to use.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The current renderer. Will not return {@link MonitorRenderer#BEST}.
 | 
			
		||||
     */
 | 
			
		||||
    public static MonitorRenderer currentRenderer() {
 | 
			
		||||
        var current = Config.monitorRenderer;
 | 
			
		||||
        if (current == MonitorRenderer.BEST) current = Config.monitorRenderer = bestRenderer();
 | 
			
		||||
        return current;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static MonitorRenderer bestRenderer() {
 | 
			
		||||
        return MonitorRenderer.VBO;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,12 @@ package dan200.computercraft.client.render.monitor;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import dan200.computercraft.client.render.BlockOutlineRenderer;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
 | 
			
		||||
import net.minecraft.client.Camera;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
 | 
			
		||||
import java.util.EnumSet;
 | 
			
		||||
 | 
			
		||||
@@ -33,8 +32,7 @@ public final class MonitorHighlightRenderer {
 | 
			
		||||
        var world = camera.getEntity().getCommandSenderWorld();
 | 
			
		||||
        var pos = hit.getBlockPos();
 | 
			
		||||
 | 
			
		||||
        var tile = world.getBlockEntity(pos);
 | 
			
		||||
        if (!(tile instanceof MonitorBlockEntity monitor)) return false;
 | 
			
		||||
        if (!(world.getBlockEntity(pos) instanceof MonitorBlockEntity monitor)) return false;
 | 
			
		||||
 | 
			
		||||
        // Determine which sides are part of the external faces of the monitor, and so which need to be rendered.
 | 
			
		||||
        var faces = EnumSet.allOf(Direction.class);
 | 
			
		||||
@@ -49,39 +47,37 @@ public final class MonitorHighlightRenderer {
 | 
			
		||||
        transformStack.pushPose();
 | 
			
		||||
        transformStack.translate(pos.getX() - cameraPos.x(), pos.getY() - cameraPos.y(), pos.getZ() - cameraPos.z());
 | 
			
		||||
 | 
			
		||||
        // I wish I could think of a better way to do this
 | 
			
		||||
        var buffer = bufferSource.getBuffer(RenderType.lines());
 | 
			
		||||
        var transform = transformStack.last().pose();
 | 
			
		||||
        var normal = transformStack.last();
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 0, UP);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, normal, 0, 0, 1, UP);
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 0, UP);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, normal, 1, 0, 1, UP);
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, EAST);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 1, EAST);
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, EAST);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 1, EAST);
 | 
			
		||||
        if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, normal, 0, 0, 0, SOUTH);
 | 
			
		||||
        if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, normal, 1, 0, 0, SOUTH);
 | 
			
		||||
        if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, normal, 0, 1, 0, SOUTH);
 | 
			
		||||
        if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, normal, 1, 1, 0, SOUTH);
 | 
			
		||||
        var transform = transformStack.last();
 | 
			
		||||
        BlockOutlineRenderer.render(bufferSource, (buffer, colour) -> draw(buffer, transform, faces, colour));
 | 
			
		||||
 | 
			
		||||
        transformStack.popPose();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void line(VertexConsumer buffer, Matrix4f transform, PoseStack.Pose normal, float x, float y, float z, Direction direction) {
 | 
			
		||||
    private static void draw(VertexConsumer buffer, PoseStack.Pose transform, EnumSet<Direction> faces, int colour) {
 | 
			
		||||
        // I wish I could think of a better way to do this
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(WEST)) line(buffer, transform, 0, 0, 0, UP, colour);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(WEST)) line(buffer, transform, 0, 0, 1, UP, colour);
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(EAST)) line(buffer, transform, 1, 0, 0, UP, colour);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(EAST)) line(buffer, transform, 1, 0, 1, UP, colour);
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 0, EAST, colour);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 1, EAST, colour);
 | 
			
		||||
        if (faces.contains(NORTH) || faces.contains(UP)) line(buffer, transform, 0, 1, 0, EAST, colour);
 | 
			
		||||
        if (faces.contains(SOUTH) || faces.contains(UP)) line(buffer, transform, 0, 1, 1, EAST, colour);
 | 
			
		||||
        if (faces.contains(WEST) || faces.contains(DOWN)) line(buffer, transform, 0, 0, 0, SOUTH, colour);
 | 
			
		||||
        if (faces.contains(EAST) || faces.contains(DOWN)) line(buffer, transform, 1, 0, 0, SOUTH, colour);
 | 
			
		||||
        if (faces.contains(WEST) || faces.contains(UP)) line(buffer, transform, 0, 1, 0, SOUTH, colour);
 | 
			
		||||
        if (faces.contains(EAST) || faces.contains(UP)) line(buffer, transform, 1, 1, 0, SOUTH, colour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void line(VertexConsumer buffer, PoseStack.Pose transform, float x, float y, float z, Direction direction, int colour) {
 | 
			
		||||
        buffer
 | 
			
		||||
            .addVertex(transform, x, y, z)
 | 
			
		||||
            .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
            .setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
 | 
			
		||||
            .setColor(colour)
 | 
			
		||||
            .setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
 | 
			
		||||
        buffer
 | 
			
		||||
            .addVertex(transform,
 | 
			
		||||
                x + direction.getStepX(),
 | 
			
		||||
                y + direction.getStepY(),
 | 
			
		||||
                z + direction.getStepZ()
 | 
			
		||||
            )
 | 
			
		||||
            .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
            .setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
 | 
			
		||||
            .addVertex(transform, x + direction.getStepX(), y + direction.getStepY(), z + direction.getStepZ())
 | 
			
		||||
            .setColor(colour)
 | 
			
		||||
            .setNormal(transform, direction.getStepX(), direction.getStepY(), direction.getStepZ());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,18 +5,12 @@
 | 
			
		||||
package dan200.computercraft.client.render.monitor;
 | 
			
		||||
 | 
			
		||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
 | 
			
		||||
import com.mojang.blaze3d.platform.GlStateManager;
 | 
			
		||||
import dan200.computercraft.client.render.vbo.DirectBuffers;
 | 
			
		||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
 | 
			
		||||
import com.mojang.blaze3d.buffers.BufferUsage;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexBuffer;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
 | 
			
		||||
import net.minecraft.core.BlockPos;
 | 
			
		||||
import org.lwjgl.opengl.GL11;
 | 
			
		||||
import org.lwjgl.opengl.GL15;
 | 
			
		||||
import org.lwjgl.opengl.GL30;
 | 
			
		||||
import org.lwjgl.opengl.GL31;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
@@ -34,52 +28,23 @@ public class MonitorRenderState implements ClientMonitor.RenderState {
 | 
			
		||||
    public long lastRenderFrame = -1;
 | 
			
		||||
    public @Nullable BlockPos lastRenderPos = null;
 | 
			
		||||
 | 
			
		||||
    public int tboBuffer;
 | 
			
		||||
    public int tboTexture;
 | 
			
		||||
    public int tboUniform;
 | 
			
		||||
    public @Nullable DirectVertexBuffer backgroundBuffer;
 | 
			
		||||
    public @Nullable DirectVertexBuffer foregroundBuffer;
 | 
			
		||||
    public @Nullable VertexBuffer backgroundBuffer;
 | 
			
		||||
    public @Nullable VertexBuffer foregroundBuffer;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create the appropriate buffer if needed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param renderer The renderer to use.
 | 
			
		||||
     * @return If a buffer was created. This will return {@code false} if we already have an appropriate buffer,
 | 
			
		||||
     * or this mode does not require one.
 | 
			
		||||
     */
 | 
			
		||||
    public boolean createBuffer(MonitorRenderer renderer) {
 | 
			
		||||
        switch (renderer) {
 | 
			
		||||
            case TBO: {
 | 
			
		||||
                if (tboBuffer != 0) return false;
 | 
			
		||||
    public boolean createBuffer() {
 | 
			
		||||
        if (backgroundBuffer != null) return false;
 | 
			
		||||
 | 
			
		||||
                deleteBuffers();
 | 
			
		||||
 | 
			
		||||
                tboBuffer = DirectBuffers.createBuffer();
 | 
			
		||||
                DirectBuffers.setEmptyBufferData(GL31.GL_TEXTURE_BUFFER, tboBuffer, GL15.GL_STATIC_DRAW);
 | 
			
		||||
                tboTexture = GlStateManager._genTexture();
 | 
			
		||||
                GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, tboTexture);
 | 
			
		||||
                GL31.glTexBuffer(GL31.GL_TEXTURE_BUFFER, GL30.GL_R8UI, tboBuffer);
 | 
			
		||||
                GL11.glBindTexture(GL31.GL_TEXTURE_BUFFER, 0);
 | 
			
		||||
 | 
			
		||||
                tboUniform = DirectBuffers.createBuffer();
 | 
			
		||||
                DirectBuffers.setEmptyBufferData(GL31.GL_UNIFORM_BUFFER, tboUniform, GL15.GL_STATIC_DRAW);
 | 
			
		||||
 | 
			
		||||
                addMonitor();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            case VBO:
 | 
			
		||||
                if (backgroundBuffer != null) return false;
 | 
			
		||||
 | 
			
		||||
                deleteBuffers();
 | 
			
		||||
                backgroundBuffer = new DirectVertexBuffer();
 | 
			
		||||
                foregroundBuffer = new DirectVertexBuffer();
 | 
			
		||||
                addMonitor();
 | 
			
		||||
                return true;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
        deleteBuffers();
 | 
			
		||||
        backgroundBuffer = new VertexBuffer(BufferUsage.STATIC_WRITE);
 | 
			
		||||
        foregroundBuffer = new VertexBuffer(BufferUsage.STATIC_WRITE);
 | 
			
		||||
        addMonitor();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addMonitor() {
 | 
			
		||||
@@ -89,21 +54,6 @@ public class MonitorRenderState implements ClientMonitor.RenderState {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void deleteBuffers() {
 | 
			
		||||
        if (tboBuffer != 0) {
 | 
			
		||||
            DirectBuffers.deleteBuffer(GL31.GL_TEXTURE_BUFFER, tboBuffer);
 | 
			
		||||
            tboBuffer = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tboTexture != 0) {
 | 
			
		||||
            GlStateManager._deleteTexture(tboTexture);
 | 
			
		||||
            tboTexture = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (tboUniform != 0) {
 | 
			
		||||
            DirectBuffers.deleteBuffer(GL31.GL_UNIFORM_BUFFER, tboUniform);
 | 
			
		||||
            tboUniform = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (backgroundBuffer != null) {
 | 
			
		||||
            backgroundBuffer.close();
 | 
			
		||||
            backgroundBuffer = null;
 | 
			
		||||
@@ -117,7 +67,7 @@ public class MonitorRenderState implements ClientMonitor.RenderState {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() {
 | 
			
		||||
        if (tboBuffer != 0 || backgroundBuffer != null) {
 | 
			
		||||
        if (backgroundBuffer != null) {
 | 
			
		||||
            synchronized (allMonitors) {
 | 
			
		||||
                allMonitors.remove(this);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render.monitor;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.shaders.Uniform;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
import dan200.computercraft.client.FrameInfo;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.core.terminal.TextBuffer;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import net.minecraft.client.renderer.ShaderInstance;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceProvider;
 | 
			
		||||
import org.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;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.getColour;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The shader used for the monitor TBO renderer.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This extends Minecraft's default shader loading code to extract out the TBO buffer and handle our custom uniforms
 | 
			
		||||
 * ({@code MonitorData}, {@code CursorBlink}).
 | 
			
		||||
 * <p>
 | 
			
		||||
 * See also {@code monitor_tbo.fsh} and {@code monitor_tbo.vsh} in the mod's resources.
 | 
			
		||||
 *
 | 
			
		||||
 * @see RenderTypes#getMonitorTextureBufferShader()
 | 
			
		||||
 */
 | 
			
		||||
public class MonitorTextureBufferShader extends ShaderInstance {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(MonitorTextureBufferShader.class);
 | 
			
		||||
 | 
			
		||||
    public static final int UNIFORM_SIZE = 4 * 4 * 16 + 4 + 4 + 2 * 4 + 4;
 | 
			
		||||
 | 
			
		||||
    static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
 | 
			
		||||
 | 
			
		||||
    private final int monitorData;
 | 
			
		||||
    private int uniformBuffer = 0;
 | 
			
		||||
 | 
			
		||||
    private final @Nullable Uniform cursorBlink;
 | 
			
		||||
 | 
			
		||||
    public MonitorTextureBufferShader(ResourceProvider provider, String location, VertexFormat format) throws IOException {
 | 
			
		||||
        super(provider, location, format);
 | 
			
		||||
        monitorData = GL31.glGetUniformBlockIndex(getId(), "MonitorData");
 | 
			
		||||
        if (monitorData == -1) throw new IllegalStateException("Could not find MonitorData uniform.");
 | 
			
		||||
 | 
			
		||||
        cursorBlink = getUniformChecked("CursorBlink");
 | 
			
		||||
 | 
			
		||||
        var tbo = getUniformChecked("Tbo");
 | 
			
		||||
        if (tbo != null) tbo.set(TEXTURE_INDEX - GL13.GL_TEXTURE0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setupUniform(int buffer) {
 | 
			
		||||
        uniformBuffer = buffer;
 | 
			
		||||
 | 
			
		||||
        var cursorAlpha = FrameInfo.getGlobalCursorBlink() ? 1 : 0;
 | 
			
		||||
        if (cursorBlink != null && cursorBlink.getIntBuffer().get(0) != cursorAlpha) cursorBlink.set(cursorAlpha);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void apply() {
 | 
			
		||||
        super.apply();
 | 
			
		||||
        GL31.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, monitorData, uniformBuffer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private Uniform getUniformChecked(String name) {
 | 
			
		||||
        var uniform = getUniform(name);
 | 
			
		||||
        if (uniform == null) {
 | 
			
		||||
            LOG.warn("Monitor shader {} should have uniform {}, but it was not present.", getName(), name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return uniform;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void setTerminalData(ByteBuffer buffer, Terminal terminal) {
 | 
			
		||||
        int width = terminal.getWidth(), height = terminal.getHeight();
 | 
			
		||||
 | 
			
		||||
        var pos = 0;
 | 
			
		||||
        for (var y = 0; y < height; y++) {
 | 
			
		||||
            TextBuffer text = terminal.getLine(y), textColour = terminal.getTextColourLine(y), background = terminal.getBackgroundColourLine(y);
 | 
			
		||||
            for (var x = 0; x < width; x++) {
 | 
			
		||||
                buffer.put(pos, (byte) (text.charAt(x) & 0xFF));
 | 
			
		||||
                buffer.put(pos + 1, (byte) getColour(textColour.charAt(x), Colour.WHITE));
 | 
			
		||||
                buffer.put(pos + 2, (byte) getColour(background.charAt(x), Colour.BLACK));
 | 
			
		||||
                pos += 3;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        buffer.limit(pos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void setUniformData(ByteBuffer buffer, Terminal terminal) {
 | 
			
		||||
        var pos = 0;
 | 
			
		||||
        var palette = terminal.getPalette();
 | 
			
		||||
        for (var i = 0; i < 16; i++) {
 | 
			
		||||
            {
 | 
			
		||||
                var colour = palette.getColour(i);
 | 
			
		||||
                if (!terminal.isColour()) {
 | 
			
		||||
                    var f = FixedWidthFontRenderer.toGreyscale(colour);
 | 
			
		||||
                    buffer.putFloat(pos, f).putFloat(pos + 4, f).putFloat(pos + 8, f);
 | 
			
		||||
                } else {
 | 
			
		||||
                    buffer.putFloat(pos, (float) colour[0]).putFloat(pos + 4, (float) colour[1]).putFloat(pos + 8, (float) colour[2]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            pos += 4 * 4; // std140 requires these are 4-wide
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var showCursor = FixedWidthFontRenderer.isCursorVisible(terminal);
 | 
			
		||||
        buffer
 | 
			
		||||
            .putInt(pos, terminal.getWidth()).putInt(pos + 4, terminal.getHeight())
 | 
			
		||||
            .putInt(pos + 8, showCursor ? terminal.getCursorX() : -2)
 | 
			
		||||
            .putInt(pos + 12, showCursor ? terminal.getCursorY() : -2)
 | 
			
		||||
            .putInt(pos + 16, 15 - terminal.getTextColour());
 | 
			
		||||
 | 
			
		||||
        buffer.limit(UNIFORM_SIZE);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,18 +4,17 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render.text;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
 | 
			
		||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.core.terminal.Palette;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.core.terminal.TextBuffer;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import org.lwjgl.system.MemoryUtil;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.ByteOrder;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.*;
 | 
			
		||||
@@ -168,40 +167,38 @@ public final class DirectFixedWidthFontRenderer {
 | 
			
		||||
    public interface QuadEmitter {
 | 
			
		||||
        VertexFormat format();
 | 
			
		||||
 | 
			
		||||
        ByteBuffer buffer();
 | 
			
		||||
 | 
			
		||||
        void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record ByteBufferEmitter(ByteBuffer buffer) implements QuadEmitter {
 | 
			
		||||
    public record ByteBufferEmitter(ByteBufferBuilder builder) implements QuadEmitter {
 | 
			
		||||
        @Override
 | 
			
		||||
        public VertexFormat format() {
 | 
			
		||||
            return RenderTypes.TERMINAL.format();
 | 
			
		||||
            return TERMINAL_TEXT.format();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void quad(float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
 | 
			
		||||
            DirectFixedWidthFontRenderer.quad(buffer, x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
 | 
			
		||||
            DirectFixedWidthFontRenderer.quad(builder, x1, y1, x2, y2, z, colour, u1, v1, u2, v2);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static void quad(ByteBuffer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
 | 
			
		||||
    static void quad(ByteBufferBuilder builder, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2) {
 | 
			
		||||
        // Emit a single quad to our buffer. This uses Unsafe (well, LWJGL's MemoryUtil) to directly blit bytes to the
 | 
			
		||||
        // underlying buffer. This allows us to have a single bounds check up-front, rather than one for every write.
 | 
			
		||||
        // This provides significant performance gains, at the cost of well, using Unsafe.
 | 
			
		||||
        // Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(rgba:BBBB)(uv1:FF)(uv2:SS),
 | 
			
		||||
        // Each vertex is 28 bytes, giving 112 bytes in total. Vertices are of the form (xyz:FFF)(abgr:BBBB)(uv1:FF)(uv2:SS),
 | 
			
		||||
        // which matches the POSITION_COLOR_TEX_LIGHTMAP vertex format.
 | 
			
		||||
 | 
			
		||||
        var position = buffer.position();
 | 
			
		||||
        var addr = MemoryUtil.memAddress(buffer);
 | 
			
		||||
        var addr = builder.reserve(112);
 | 
			
		||||
 | 
			
		||||
        // We're doing terrible unsafe hacks below, so let's be really sure that what we're doing is reasonable.
 | 
			
		||||
        if (position < 0 || 112 > buffer.limit() - position) throw new IndexOutOfBoundsException();
 | 
			
		||||
        // Require the pointer to be aligned to a 32-bit boundary.
 | 
			
		||||
        if ((addr & 3) != 0) throw new IllegalStateException("Memory is not aligned");
 | 
			
		||||
        if (TERMINAL_TEXT.format().getVertexSize() != 28) {
 | 
			
		||||
            throw new IllegalStateException("Incorrect vertex size");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Pack colour so it is equivalent to rgba:BBBB. This matches the logic in BufferBuilder.
 | 
			
		||||
        var colourAbgr = FastColor.ABGR32.fromArgb32(colour);
 | 
			
		||||
        var colourAbgr = ARGB.toABGR(colour);
 | 
			
		||||
        // Pack colour so it is equivalent to abgr:BBBB. This matches the logic in BufferBuilder.
 | 
			
		||||
        var nativeColour = IS_LITTLE_ENDIAN ? colourAbgr : Integer.reverseBytes(colourAbgr);
 | 
			
		||||
 | 
			
		||||
        memPutFloat(addr + 0, x1);
 | 
			
		||||
@@ -240,9 +237,6 @@ public final class DirectFixedWidthFontRenderer {
 | 
			
		||||
        memPutShort(addr + 108, (short) 0xF0);
 | 
			
		||||
        memPutShort(addr + 110, (short) 0xF0);
 | 
			
		||||
 | 
			
		||||
        // Finally increment the position.
 | 
			
		||||
        buffer.position(position + 112);
 | 
			
		||||
 | 
			
		||||
        // Well done for getting to the end of this method. I recommend you take a break and go look at cute puppies.
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,13 @@ import dan200.computercraft.core.terminal.Palette;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
import dan200.computercraft.core.terminal.TextBuffer;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import net.minecraft.client.renderer.LightTexture;
 | 
			
		||||
import net.minecraft.client.renderer.RenderType;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.util.FastColor;
 | 
			
		||||
import net.minecraft.util.ARGB;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
import org.joml.Vector3f;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles rendering fixed width text and computer terminals.
 | 
			
		||||
 * <p>
 | 
			
		||||
@@ -33,7 +33,12 @@ import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMA
 | 
			
		||||
 * {@link DirectFixedWidthFontRenderer}.
 | 
			
		||||
 */
 | 
			
		||||
public final class FixedWidthFontRenderer {
 | 
			
		||||
    public static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
 | 
			
		||||
    private static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A render type for terminal text.
 | 
			
		||||
     */
 | 
			
		||||
    public static final RenderType TERMINAL_TEXT = RenderType.text(FONT);
 | 
			
		||||
 | 
			
		||||
    public static final int FONT_HEIGHT = 9;
 | 
			
		||||
    public static final int FONT_WIDTH = 6;
 | 
			
		||||
@@ -42,8 +47,8 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
    static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH;
 | 
			
		||||
    static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
 | 
			
		||||
 | 
			
		||||
    private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
 | 
			
		||||
    private static final float Z_OFFSET = 1e-3f;
 | 
			
		||||
    private static final int BLACK = ARGB.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
 | 
			
		||||
    private static final float Z_OFFSET = 1e-4f;
 | 
			
		||||
 | 
			
		||||
    private FixedWidthFontRenderer() {
 | 
			
		||||
    }
 | 
			
		||||
@@ -137,7 +142,7 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
            var rowY = y + FONT_HEIGHT * i;
 | 
			
		||||
            drawString(
 | 
			
		||||
                emitter, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
 | 
			
		||||
                palette, FULL_BRIGHT_LIGHTMAP
 | 
			
		||||
                palette, LightTexture.FULL_BRIGHT
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -152,12 +157,12 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
        // Top and bottom margins
 | 
			
		||||
        drawBackground(
 | 
			
		||||
            emitter, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
 | 
			
		||||
            leftMarginSize, rightMarginSize, topMarginSize, FULL_BRIGHT_LIGHTMAP
 | 
			
		||||
            leftMarginSize, rightMarginSize, topMarginSize, LightTexture.FULL_BRIGHT
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        drawBackground(
 | 
			
		||||
            emitter, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
 | 
			
		||||
            leftMarginSize, rightMarginSize, bottomMarginSize, FULL_BRIGHT_LIGHTMAP
 | 
			
		||||
            leftMarginSize, rightMarginSize, bottomMarginSize, LightTexture.FULL_BRIGHT
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // The main text
 | 
			
		||||
@@ -165,7 +170,7 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
            var rowY = y + FONT_HEIGHT * i;
 | 
			
		||||
            drawBackground(
 | 
			
		||||
                emitter, x, rowY, terminal.getBackgroundColourLine(i), palette,
 | 
			
		||||
                leftMarginSize, rightMarginSize, FONT_HEIGHT, FULL_BRIGHT_LIGHTMAP
 | 
			
		||||
                leftMarginSize, rightMarginSize, FONT_HEIGHT, LightTexture.FULL_BRIGHT
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -181,7 +186,7 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
    public static void drawCursor(QuadEmitter emitter, float x, float y, Terminal terminal) {
 | 
			
		||||
        if (isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()) {
 | 
			
		||||
            var colour = terminal.getPalette().getRenderColours(15 - terminal.getTextColour());
 | 
			
		||||
            drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, FULL_BRIGHT_LIGHTMAP);
 | 
			
		||||
            drawChar(emitter, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, LightTexture.FULL_BRIGHT);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -207,7 +212,7 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void drawEmptyTerminal(QuadEmitter emitter, float x, float y, float width, float height) {
 | 
			
		||||
        drawQuad(emitter, x, y, 0, width, height, BLACK, FULL_BRIGHT_LIGHTMAP);
 | 
			
		||||
        drawQuad(emitter, x, y, 0, width, height, BLACK, LightTexture.FULL_BRIGHT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public record QuadEmitter(Matrix4f poseMatrix, VertexConsumer consumer) {
 | 
			
		||||
@@ -220,11 +225,10 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
    private static void quad(QuadEmitter c, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2, int light) {
 | 
			
		||||
        var poseMatrix = c.poseMatrix();
 | 
			
		||||
        var consumer = c.consumer();
 | 
			
		||||
        int r = FastColor.ARGB32.red(colour), g = FastColor.ARGB32.green(colour), b = FastColor.ARGB32.blue(colour), a = FastColor.ARGB32.alpha(colour);
 | 
			
		||||
 | 
			
		||||
        consumer.addVertex(poseMatrix, x1, y1, z).setColor(r, g, b, a).setUv(u1, v1).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x1, y2, z).setColor(r, g, b, a).setUv(u1, v2).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x2, y2, z).setColor(r, g, b, a).setUv(u2, v2).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x2, y1, z).setColor(r, g, b, a).setUv(u2, v1).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x1, y2, z).setColor(colour).setUv(u1, v2).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x2, y2, z).setColor(colour).setUv(u2, v2).setLight(light);
 | 
			
		||||
        consumer.addVertex(poseMatrix, x2, y1, z).setColor(colour).setUv(u2, v1).setLight(light);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render.vbo;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.platform.GlStateManager;
 | 
			
		||||
import com.mojang.blaze3d.systems.RenderSystem;
 | 
			
		||||
import com.mojang.blaze3d.vertex.BufferUploader;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import org.lwjgl.opengl.GL;
 | 
			
		||||
import org.lwjgl.opengl.GL15C;
 | 
			
		||||
import org.lwjgl.opengl.GL45C;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides utilities to interact with OpenGL's buffer objects, either using direct state access or binding/unbinding
 | 
			
		||||
 * it.
 | 
			
		||||
 */
 | 
			
		||||
public class DirectBuffers {
 | 
			
		||||
    public static final boolean HAS_DSA;
 | 
			
		||||
    static final boolean ON_LINUX = Util.getPlatform() == Util.OS.LINUX;
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        var capabilities = GL.getCapabilities();
 | 
			
		||||
        HAS_DSA = capabilities.OpenGL45 || capabilities.GL_ARB_direct_state_access;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static int createBuffer() {
 | 
			
		||||
        return HAS_DSA ? GL45C.glCreateBuffers() : GL15C.glGenBuffers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a previously created buffer.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * On Linux, {@link GlStateManager#_glDeleteBuffers(int)} clears a buffer before deleting it. However, this involves
 | 
			
		||||
     * binding and unbinding the buffer, conflicting with {@link BufferUploader}'s cache. This deletion method uses
 | 
			
		||||
     * our existing {@link #setEmptyBufferData(int, int, int)}, which correctly handles clearing the buffer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param type The buffer's type.
 | 
			
		||||
     * @param id   The buffer's ID.
 | 
			
		||||
     */
 | 
			
		||||
    public static void deleteBuffer(int type, int id) {
 | 
			
		||||
        RenderSystem.assertOnRenderThread();
 | 
			
		||||
        if (ON_LINUX) DirectBuffers.setEmptyBufferData(type, id, GL15C.GL_DYNAMIC_DRAW);
 | 
			
		||||
        GL15C.glDeleteBuffers(id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void setBufferData(int type, int id, ByteBuffer buffer, int flags) {
 | 
			
		||||
        if (HAS_DSA) {
 | 
			
		||||
            GL45C.glNamedBufferData(id, buffer, flags);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
 | 
			
		||||
            GlStateManager._glBindBuffer(type, id);
 | 
			
		||||
            GlStateManager._glBufferData(type, buffer, flags);
 | 
			
		||||
            GlStateManager._glBindBuffer(type, 0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void setEmptyBufferData(int type, int id, int flags) {
 | 
			
		||||
        if (HAS_DSA) {
 | 
			
		||||
            GL45C.glNamedBufferData(id, 0, flags);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (type == GL15C.GL_ARRAY_BUFFER) BufferUploader.reset();
 | 
			
		||||
            GlStateManager._glBindBuffer(type, id);
 | 
			
		||||
            GlStateManager._glBufferData(type, 0, flags);
 | 
			
		||||
            GlStateManager._glBindBuffer(type, 0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render.vbo;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.systems.RenderSystem;
 | 
			
		||||
import com.mojang.blaze3d.vertex.BufferUploader;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexBuffer;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
import net.minecraft.client.renderer.ShaderInstance;
 | 
			
		||||
import org.joml.Matrix4f;
 | 
			
		||||
import org.lwjgl.opengl.GL15;
 | 
			
		||||
import org.lwjgl.opengl.GL15C;
 | 
			
		||||
import org.lwjgl.opengl.GL45C;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A version of {@link VertexBuffer} which allows uploading {@link ByteBuffer}s directly.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This should probably be its own class (rather than subclassing), but I need access to {@link VertexBuffer#drawWithShader}.
 | 
			
		||||
 */
 | 
			
		||||
public class DirectVertexBuffer extends VertexBuffer {
 | 
			
		||||
    private int actualIndexCount;
 | 
			
		||||
 | 
			
		||||
    public DirectVertexBuffer() {
 | 
			
		||||
        super(Usage.STATIC);
 | 
			
		||||
        if (DirectBuffers.HAS_DSA) {
 | 
			
		||||
            RenderSystem.glDeleteBuffers(vertexBufferId);
 | 
			
		||||
            if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
 | 
			
		||||
            vertexBufferId = GL45C.glCreateBuffers();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void upload(int vertexCount, VertexFormat.Mode mode, VertexFormat format, ByteBuffer buffer) {
 | 
			
		||||
        bind();
 | 
			
		||||
 | 
			
		||||
        this.mode = mode;
 | 
			
		||||
        actualIndexCount = indexCount = mode.indexCount(vertexCount);
 | 
			
		||||
        indexType = VertexFormat.IndexType.SHORT;
 | 
			
		||||
 | 
			
		||||
        RenderSystem.assertOnRenderThread();
 | 
			
		||||
 | 
			
		||||
        DirectBuffers.setBufferData(GL15.GL_ARRAY_BUFFER, vertexBufferId, buffer, GL15.GL_STATIC_DRAW);
 | 
			
		||||
        if (format != this.format) {
 | 
			
		||||
            if (this.format != null) this.format.clearBufferState();
 | 
			
		||||
            this.format = format;
 | 
			
		||||
 | 
			
		||||
            GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, vertexBufferId);
 | 
			
		||||
            format.setupBufferState();
 | 
			
		||||
            GL15C.glBindBuffer(GL15C.GL_ARRAY_BUFFER, 0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var indexBuffer = RenderSystem.getSequentialBuffer(mode);
 | 
			
		||||
        if (indexBuffer != sequentialIndices || !indexBuffer.hasStorage(indexCount)) {
 | 
			
		||||
            indexBuffer.bind(indexCount);
 | 
			
		||||
            sequentialIndices = indexBuffer;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void drawWithShader(Matrix4f modelView, Matrix4f projection, ShaderInstance shader, int indexCount) {
 | 
			
		||||
        this.indexCount = indexCount;
 | 
			
		||||
        drawWithShader(modelView, projection, shader);
 | 
			
		||||
        this.indexCount = actualIndexCount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getIndexCount() {
 | 
			
		||||
        return actualIndexCount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() {
 | 
			
		||||
        super.close();
 | 
			
		||||
        if (DirectBuffers.ON_LINUX) BufferUploader.reset(); // See comment on DirectBuffers.deleteBuffer.
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@
 | 
			
		||||
package dan200.computercraft.client.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.client.ModelLocation;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
@@ -15,7 +14,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;
 | 
			
		||||
 | 
			
		||||
@@ -39,23 +38,23 @@ public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record ModemModels(
 | 
			
		||||
        ModelLocation leftOffModel, ModelLocation rightOffModel,
 | 
			
		||||
        ModelLocation leftOnModel, ModelLocation rightOnModel
 | 
			
		||||
        ResourceLocation leftOffModel, ResourceLocation rightOffModel,
 | 
			
		||||
        ResourceLocation leftOnModel, ResourceLocation rightOnModel
 | 
			
		||||
    ) {
 | 
			
		||||
        private static final ModemModels NORMAL = create("normal");
 | 
			
		||||
        private static final ModemModels ADVANCED = create("advanced");
 | 
			
		||||
 | 
			
		||||
        public static ModemModels create(String type) {
 | 
			
		||||
            return new ModemModels(
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left")),
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right")),
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left")),
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right"))
 | 
			
		||||
                ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left"),
 | 
			
		||||
                ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right"),
 | 
			
		||||
                ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left"),
 | 
			
		||||
                ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right")
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
            return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel).flatMap(ModelLocation::getDependencies);
 | 
			
		||||
            return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.turtle;
 | 
			
		||||
 | 
			
		||||
import com.mojang.math.Transformation;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
@@ -25,7 +24,7 @@ import java.util.stream.Stream;
 | 
			
		||||
 */
 | 
			
		||||
public final class TurtleUpgradeModellers {
 | 
			
		||||
    private static final TurtleUpgradeModeller<ITurtleUpgrade> NULL_TURTLE_MODELLER = (upgrade, turtle, side, data) ->
 | 
			
		||||
        new TransformedModel(Minecraft.getInstance().getModelManager().getMissingModel(), Transformation.identity());
 | 
			
		||||
        TransformedModel.of(Minecraft.getInstance().getModelManager().getMissingModel());
 | 
			
		||||
 | 
			
		||||
    private static final Map<UpgradeType<? extends ITurtleUpgrade>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
 | 
			
		||||
    private static volatile boolean fetchedModels;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,11 +8,16 @@ 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.BlockModelProvider;
 | 
			
		||||
import dan200.computercraft.data.client.ExtraModelsProvider;
 | 
			
		||||
import dan200.computercraft.data.client.ItemModelProvider;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.client.data.models.BlockModelGenerators;
 | 
			
		||||
import net.minecraft.client.data.models.ItemModelGenerators;
 | 
			
		||||
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
 | 
			
		||||
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
 | 
			
		||||
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
 | 
			
		||||
@@ -23,7 +28,6 @@ import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.data.registries.RegistryPatchGenerator;
 | 
			
		||||
import net.minecraft.data.tags.TagsProvider;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.PackType;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
 | 
			
		||||
@@ -56,23 +60,27 @@ public final class DataProviders {
 | 
			
		||||
        var fullRegistries = fullRegistryPatch.thenApply(RegistrySetBuilder.PatchedRegistries::full);
 | 
			
		||||
 | 
			
		||||
        generator.registries(fullRegistryPatch);
 | 
			
		||||
        generator.add(out -> new RecipeProvider(out, fullRegistries));
 | 
			
		||||
        generator.add(out -> new RecipeProvider.Runner(out, fullRegistries));
 | 
			
		||||
 | 
			
		||||
        var blockTags = generator.blockTags(TagProvider::blockTags);
 | 
			
		||||
        generator.itemTags(TagProvider::itemTags, blockTags);
 | 
			
		||||
 | 
			
		||||
        generator.add(out -> new net.minecraft.data.loot.LootTableProvider(out, Set.of(), LootTableProvider.getTables(), fullRegistries));
 | 
			
		||||
 | 
			
		||||
        generator.add(out -> new ModelProvider(out, BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels));
 | 
			
		||||
 | 
			
		||||
        generator.add(out -> new LanguageProvider(out, fullRegistries));
 | 
			
		||||
 | 
			
		||||
        generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
 | 
			
		||||
        generator.addFromCodec("Block atlases", PackOutput.Target.RESOURCE_PACK, "atlases", SpriteSources.FILE_CODEC, out -> {
 | 
			
		||||
            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(ResourceLocation.withDefaultNamespace("gui"), makeSprites(Stream.of(
 | 
			
		||||
                UpgradeSlot.LEFT_UPGRADE,
 | 
			
		||||
                UpgradeSlot.RIGHT_UPGRADE
 | 
			
		||||
            )));
 | 
			
		||||
 | 
			
		||||
            out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
 | 
			
		||||
                // Computers
 | 
			
		||||
                GuiSprites.COMPUTER_NORMAL.textures(),
 | 
			
		||||
@@ -88,6 +96,8 @@ public final class DataProviders {
 | 
			
		||||
                return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        generator.addModels(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SafeVarargs
 | 
			
		||||
@@ -101,7 +111,7 @@ public final class DataProviders {
 | 
			
		||||
 | 
			
		||||
        <T extends DataProvider> T add(DataProvider.Factory<T> factory);
 | 
			
		||||
 | 
			
		||||
        <T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
 | 
			
		||||
        <T> void addFromCodec(String name, PackOutput.Target target, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
 | 
			
		||||
 | 
			
		||||
        TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags);
 | 
			
		||||
 | 
			
		||||
@@ -113,5 +123,14 @@ public final class DataProviders {
 | 
			
		||||
         * @param registries The patched registries to write.
 | 
			
		||||
         */
 | 
			
		||||
        void registries(CompletableFuture<RegistrySetBuilder.PatchedRegistries> registries);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Generate block and item models.
 | 
			
		||||
         *
 | 
			
		||||
         * @param blocks The generator for block states and models.
 | 
			
		||||
         * @param items  The generator for item models.
 | 
			
		||||
         * @see net.minecraft.client.data.models.ModelProvider
 | 
			
		||||
         */
 | 
			
		||||
        void addModels(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,97 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import net.minecraft.data.models.ItemModelGenerators;
 | 
			
		||||
import net.minecraft.data.models.model.ModelTemplate;
 | 
			
		||||
import net.minecraft.data.models.model.ModelTemplates;
 | 
			
		||||
import net.minecraft.data.models.model.TextureMapping;
 | 
			
		||||
import net.minecraft.data.models.model.TextureSlot;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocation;
 | 
			
		||||
 | 
			
		||||
public final class ItemModelProvider {
 | 
			
		||||
    private ItemModelProvider() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void addItemModels(ItemModelGenerators generators) {
 | 
			
		||||
        registerDisk(generators, ModRegistry.Items.DISK.get());
 | 
			
		||||
        registerDisk(generators, ModRegistry.Items.TREASURE_DISK.get());
 | 
			
		||||
 | 
			
		||||
        registerPocketComputer(generators, getModelLocation(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), false);
 | 
			
		||||
        registerPocketComputer(generators, getModelLocation(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get()), false);
 | 
			
		||||
        registerPocketComputer(generators, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_colour"), true);
 | 
			
		||||
 | 
			
		||||
        generators.generateFlatItem(ModRegistry.Items.PRINTED_BOOK.get(), ModelTemplates.FLAT_ITEM);
 | 
			
		||||
        generators.generateFlatItem(ModRegistry.Items.PRINTED_PAGE.get(), ModelTemplates.FLAT_ITEM);
 | 
			
		||||
        generators.generateFlatItem(ModRegistry.Items.PRINTED_PAGES.get(), ModelTemplates.FLAT_ITEM);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerPocketComputer(ItemModelGenerators generators, ResourceLocation id, boolean off) {
 | 
			
		||||
        createFlatItem(generators, id.withSuffix("_blinking"),
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_blink"),
 | 
			
		||||
            id,
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_light")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        createFlatItem(generators, id.withSuffix("_on"),
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_on"),
 | 
			
		||||
            id,
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_light")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Don't emit the default/off state for advanced/normal pocket computers, as they have item overrides.
 | 
			
		||||
        if (off) {
 | 
			
		||||
            createFlatItem(generators, id,
 | 
			
		||||
                ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/pocket_computer_frame"),
 | 
			
		||||
                id
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerDisk(ItemModelGenerators generators, Item item) {
 | 
			
		||||
        createFlatItem(generators, item,
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/disk_frame"),
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item/disk_colour")
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void createFlatItem(ItemModelGenerators generators, Item item, ResourceLocation... ids) {
 | 
			
		||||
        createFlatItem(generators, getModelLocation(item), ids);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Generate a flat item from an arbitrary number of layers.
 | 
			
		||||
     *
 | 
			
		||||
     * @param generators The current item generator helper.
 | 
			
		||||
     * @param model      The model we're writing to.
 | 
			
		||||
     * @param textures   The textures which make up this model.
 | 
			
		||||
     * @see net.minecraft.client.renderer.block.model.ItemModelGenerator The parser for this file format.
 | 
			
		||||
     */
 | 
			
		||||
    private static void createFlatItem(ItemModelGenerators generators, ResourceLocation model, ResourceLocation... textures) {
 | 
			
		||||
        if (textures.length > 5) throw new IndexOutOfBoundsException("Too many layers");
 | 
			
		||||
        if (textures.length == 0) throw new IndexOutOfBoundsException("Must have at least one texture");
 | 
			
		||||
        if (textures.length == 1) {
 | 
			
		||||
            ModelTemplates.FLAT_ITEM.create(model, TextureMapping.layer0(textures[0]), generators.output);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var slots = new TextureSlot[textures.length];
 | 
			
		||||
        var mapping = new TextureMapping();
 | 
			
		||||
        for (var i = 0; i < textures.length; i++) {
 | 
			
		||||
            var slot = slots[i] = TextureSlot.create("layer" + i);
 | 
			
		||||
            mapping.put(slot, textures[i]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        new ModelTemplate(Optional.of(ResourceLocation.withDefaultNamespace("item/generated")), Optional.empty(), slots)
 | 
			
		||||
            .create(model, mapping, generators.output);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -278,19 +278,18 @@ public final class LanguageProvider implements DataProvider {
 | 
			
		||||
        addConfigEntry(ConfigSpec.monitorWidth, "Max monitor width");
 | 
			
		||||
        addConfigEntry(ConfigSpec.monitorHeight, "Max monitor height");
 | 
			
		||||
 | 
			
		||||
        addConfigEntry(ConfigSpec.monitorRenderer, "Monitor renderer");
 | 
			
		||||
        addConfigEntry(ConfigSpec.monitorDistance, "Monitor distance");
 | 
			
		||||
        addConfigEntry(ConfigSpec.uploadNagDelay, "Upload nag delay");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Stream<String> getExpectedKeys(HolderLookup.Provider registries) {
 | 
			
		||||
        return Stream.of(
 | 
			
		||||
            BuiltInRegistries.BLOCK.holders()
 | 
			
		||||
            BuiltInRegistries.BLOCK.listElements()
 | 
			
		||||
                .filter(x -> x.key().location().getNamespace().equals(ComputerCraftAPI.MOD_ID))
 | 
			
		||||
                .map(x -> x.value().getDescriptionId())
 | 
			
		||||
                // Exclude blocks that just reuse vanilla translations, such as the lectern.
 | 
			
		||||
                .filter(x -> !x.startsWith("block.minecraft.")),
 | 
			
		||||
            BuiltInRegistries.ITEM.holders()
 | 
			
		||||
            BuiltInRegistries.ITEM.listElements()
 | 
			
		||||
                .filter(x -> x.key().location().getNamespace().equals(ComputerCraftAPI.MOD_ID))
 | 
			
		||||
                .map(x -> x.value().getDescriptionId()),
 | 
			
		||||
            registries.lookupOrThrow(ITurtleUpgrade.REGISTRY).listElements().flatMap(x -> getTranslationKeys(x.value().getAdjective())),
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ class LootTableProvider {
 | 
			
		||||
 | 
			
		||||
        blockDrop(add, ModRegistry.Blocks.LECTERN, LootItem.lootTableItem(Items.LECTERN), ExplosionCondition.survivesExplosion());
 | 
			
		||||
 | 
			
		||||
        add.accept(ModRegistry.Blocks.CABLE.get().getLootTable(), LootTable
 | 
			
		||||
        add.accept(ModRegistry.Blocks.CABLE.get().getLootTable().orElseThrow(), LootTable
 | 
			
		||||
            .lootTable()
 | 
			
		||||
            .withPool(LootPool.lootPool()
 | 
			
		||||
                .setRolls(ConstantValue.exactly(1))
 | 
			
		||||
@@ -115,7 +115,7 @@ class LootTableProvider {
 | 
			
		||||
        LootItemCondition.Builder condition
 | 
			
		||||
    ) {
 | 
			
		||||
        var block = wrapper.get();
 | 
			
		||||
        add.accept(block.getLootTable(), LootTable
 | 
			
		||||
        add.accept(block.getLootTable().orElseThrow(), LootTable
 | 
			
		||||
            .lootTable()
 | 
			
		||||
            .withPool(LootPool.lootPool()
 | 
			
		||||
                .setRolls(ConstantValue.exactly(1))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,101 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonElement;
 | 
			
		||||
import dan200.computercraft.shared.util.RegistryHelper;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.core.registries.BuiltInRegistries;
 | 
			
		||||
import net.minecraft.data.CachedOutput;
 | 
			
		||||
import net.minecraft.data.DataProvider;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.data.models.BlockModelGenerators;
 | 
			
		||||
import net.minecraft.data.models.ItemModelGenerators;
 | 
			
		||||
import net.minecraft.data.models.blockstates.BlockStateGenerator;
 | 
			
		||||
import net.minecraft.data.models.model.DelegatedModel;
 | 
			
		||||
import net.minecraft.data.models.model.ModelLocationUtils;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A copy of {@link net.minecraft.data.models.ModelProvider} which accepts a custom generator.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Please don't sue me Mojang. Or at least make these changes to vanilla before doing so!
 | 
			
		||||
 */
 | 
			
		||||
public class ModelProvider implements DataProvider {
 | 
			
		||||
    private final PackOutput.PathProvider blockStatePath;
 | 
			
		||||
    private final PackOutput.PathProvider modelPath;
 | 
			
		||||
 | 
			
		||||
    private final Consumer<BlockModelGenerators> blocks;
 | 
			
		||||
    private final Consumer<ItemModelGenerators> items;
 | 
			
		||||
 | 
			
		||||
    public ModelProvider(PackOutput output, Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) {
 | 
			
		||||
        blockStatePath = output.createPathProvider(PackOutput.Target.RESOURCE_PACK, "blockstates");
 | 
			
		||||
        modelPath = output.createPathProvider(PackOutput.Target.RESOURCE_PACK, "models");
 | 
			
		||||
 | 
			
		||||
        this.blocks = blocks;
 | 
			
		||||
        this.items = items;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public CompletableFuture<?> run(CachedOutput output) {
 | 
			
		||||
        Map<Block, BlockStateGenerator> blockStates = new HashMap<>();
 | 
			
		||||
        Consumer<BlockStateGenerator> addBlockState = generator -> {
 | 
			
		||||
            var block = generator.getBlock();
 | 
			
		||||
            if (blockStates.containsKey(block)) {
 | 
			
		||||
                throw new IllegalStateException("Duplicate blockstate definition for " + block);
 | 
			
		||||
            }
 | 
			
		||||
            blockStates.put(block, generator);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Map<ResourceLocation, Supplier<JsonElement>> models = new HashMap<>();
 | 
			
		||||
        BiConsumer<ResourceLocation, Supplier<JsonElement>> addModel = (id, contents) -> {
 | 
			
		||||
            if (models.containsKey(id)) throw new IllegalStateException("Duplicate model definition for " + id);
 | 
			
		||||
            models.put(id, contents);
 | 
			
		||||
        };
 | 
			
		||||
        Set<Item> explicitItems = new HashSet<>();
 | 
			
		||||
        blocks.accept(new BlockModelGenerators(addBlockState, addModel, explicitItems::add));
 | 
			
		||||
        items.accept(new ItemModelGenerators(addModel));
 | 
			
		||||
 | 
			
		||||
        for (var block : BuiltInRegistries.BLOCK) {
 | 
			
		||||
            if (!blockStates.containsKey(block)) continue;
 | 
			
		||||
 | 
			
		||||
            var item = Item.BY_BLOCK.get(block);
 | 
			
		||||
            if (item == null || explicitItems.contains(item)) continue;
 | 
			
		||||
 | 
			
		||||
            var model = ModelLocationUtils.getModelLocation(item);
 | 
			
		||||
            if (!models.containsKey(model)) {
 | 
			
		||||
                models.put(model, new DelegatedModel(ModelLocationUtils.getModelLocation(block)));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<CompletableFuture<?>> futures = new ArrayList<>();
 | 
			
		||||
        saveCollection(output, futures, blockStates, x -> blockStatePath.json(RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK, x)));
 | 
			
		||||
        saveCollection(output, futures, models, modelPath::json);
 | 
			
		||||
        return Util.sequenceFailFast(futures);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private <T> void saveCollection(CachedOutput output, List<CompletableFuture<?>> futures, Map<T, ? extends Supplier<JsonElement>> items, Function<T, Path> getLocation) {
 | 
			
		||||
        for (Map.Entry<T, ? extends Supplier<JsonElement>> entry : items.entrySet()) {
 | 
			
		||||
            var path = getLocation.apply(entry.getKey());
 | 
			
		||||
 | 
			
		||||
            futures.add(DataProvider.saveStable(output, entry.getValue().get(), path));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getName() {
 | 
			
		||||
        return "Block State Definitions";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,14 +4,11 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.mojang.authlib.GameProfile;
 | 
			
		||||
import com.mojang.serialization.JsonOps;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeData;
 | 
			
		||||
import dan200.computercraft.core.util.Colour;
 | 
			
		||||
import dan200.computercraft.data.recipe.ShapedSpecBuilder;
 | 
			
		||||
import dan200.computercraft.data.recipe.ShapelessSpecBuilder;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
@@ -24,7 +21,6 @@ import dan200.computercraft.shared.platform.RecipeIngredients;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.ImpostorShapelessRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.TransformShapedRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.TransformShapelessRecipe;
 | 
			
		||||
import dan200.computercraft.shared.recipe.function.CopyComponents;
 | 
			
		||||
@@ -37,6 +33,7 @@ import dan200.computercraft.shared.util.RegistryHelper;
 | 
			
		||||
import net.minecraft.advancements.Criterion;
 | 
			
		||||
import net.minecraft.advancements.critereon.InventoryChangeTrigger;
 | 
			
		||||
import net.minecraft.advancements.critereon.ItemPredicate;
 | 
			
		||||
import net.minecraft.core.HolderGetter;
 | 
			
		||||
import net.minecraft.core.HolderLookup;
 | 
			
		||||
import net.minecraft.core.component.DataComponents;
 | 
			
		||||
import net.minecraft.core.registries.BuiltInRegistries;
 | 
			
		||||
@@ -44,15 +41,14 @@ import net.minecraft.core.registries.Registries;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.data.recipes.RecipeCategory;
 | 
			
		||||
import net.minecraft.data.recipes.RecipeOutput;
 | 
			
		||||
import net.minecraft.data.recipes.ShapedRecipeBuilder;
 | 
			
		||||
import net.minecraft.data.recipes.ShapelessRecipeBuilder;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.tags.ItemTags;
 | 
			
		||||
import net.minecraft.tags.TagKey;
 | 
			
		||||
import net.minecraft.util.GsonHelper;
 | 
			
		||||
import net.minecraft.world.item.*;
 | 
			
		||||
import net.minecraft.world.item.component.DyedItemColor;
 | 
			
		||||
import net.minecraft.world.item.DyeColor;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.Items;
 | 
			
		||||
import net.minecraft.world.item.component.ResolvableProfile;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingBookCategory;
 | 
			
		||||
import net.minecraft.world.item.crafting.Ingredient;
 | 
			
		||||
@@ -62,68 +58,46 @@ import net.minecraft.world.level.ItemLike;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.api.ComputerCraftTags.Items.COMPUTER;
 | 
			
		||||
import static dan200.computercraft.api.ComputerCraftTags.Items.WIRED_MODEM;
 | 
			
		||||
 | 
			
		||||
final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
    private final RecipeIngredients ingredients = PlatformHelper.get().getRecipeIngredients();
 | 
			
		||||
    private final RecipeIngredients ingredients;
 | 
			
		||||
    private final HolderGetter<Item> items;
 | 
			
		||||
 | 
			
		||||
    private final CompletableFuture<HolderLookup.Provider> registries;
 | 
			
		||||
 | 
			
		||||
    RecipeProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
 | 
			
		||||
        super(output, registries);
 | 
			
		||||
        this.registries = registries;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private HolderLookup.Provider registries() {
 | 
			
		||||
        try {
 | 
			
		||||
            return registries.get();
 | 
			
		||||
        } catch (InterruptedException e) {
 | 
			
		||||
            Thread.currentThread().interrupt();
 | 
			
		||||
            throw new RuntimeException("Interrupted");
 | 
			
		||||
        } catch (ExecutionException e) {
 | 
			
		||||
            var cause = e.getCause();
 | 
			
		||||
            throw cause instanceof RuntimeException rt ? rt : new RuntimeException("Unexpected error", cause);
 | 
			
		||||
        }
 | 
			
		||||
    RecipeProvider(HolderLookup.Provider registries, RecipeOutput recipeOutput) {
 | 
			
		||||
        super(registries, recipeOutput);
 | 
			
		||||
        this.items = registries.lookupOrThrow(Registries.ITEM);
 | 
			
		||||
        ingredients = PlatformHelper.get().getRecipeIngredients();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void buildRecipes(RecipeOutput add) {
 | 
			
		||||
        var registries = registries();
 | 
			
		||||
    public void buildRecipes() {
 | 
			
		||||
        basicRecipes();
 | 
			
		||||
        diskColours();
 | 
			
		||||
        pocketUpgrades();
 | 
			
		||||
        turtleUpgrades();
 | 
			
		||||
        turtleOverlays();
 | 
			
		||||
 | 
			
		||||
        basicRecipes(add);
 | 
			
		||||
        diskColours(add);
 | 
			
		||||
        pocketUpgrades(add, registries);
 | 
			
		||||
        turtleUpgrades(add, registries);
 | 
			
		||||
        turtleOverlays(add, registries);
 | 
			
		||||
 | 
			
		||||
        addSpecial(add, new DiskRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        addSpecial(add, new ColourableRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        addSpecial(add, new ClearColourRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        addSpecial(add, new TurtleUpgradeRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        addSpecial(add, new PocketComputerUpgradeRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        special(new ColourableRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        special(new ClearColourRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        special(new TurtleUpgradeRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
        special(new PocketComputerUpgradeRecipe(CraftingBookCategory.MISC));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a crafting recipe for a disk of every dye colour.
 | 
			
		||||
     *
 | 
			
		||||
     * @param output The callback to add recipes.
 | 
			
		||||
     * Register a disk recipe.
 | 
			
		||||
     */
 | 
			
		||||
    private void diskColours(RecipeOutput output) {
 | 
			
		||||
        for (var colour : Colour.VALUES) {
 | 
			
		||||
            ShapelessSpecBuilder
 | 
			
		||||
                .shapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(ModRegistry.Items.DISK.get(), DataComponents.DYED_COLOR, new DyedItemColor(colour.getHex(), false)))
 | 
			
		||||
                .requires(ingredients.redstone())
 | 
			
		||||
                .requires(Items.PAPER)
 | 
			
		||||
                .requires(DyeItem.byColor(ofColour(colour)))
 | 
			
		||||
                .group("computercraft:disk")
 | 
			
		||||
                .unlockedBy("has_drive", inventoryChange(ModRegistry.Items.DISK_DRIVE.get()))
 | 
			
		||||
                .build(ImpostorShapelessRecipe::new)
 | 
			
		||||
                .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "disk_" + (colour.ordinal() + 1)));
 | 
			
		||||
        }
 | 
			
		||||
    private void diskColours() {
 | 
			
		||||
        customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.DISK.get())
 | 
			
		||||
            .requires(ingredients.redstone())
 | 
			
		||||
            .requires(Items.PAPER)
 | 
			
		||||
            .group("computercraft:disk")
 | 
			
		||||
            .unlockedBy("has_drive", has(ModRegistry.Items.DISK_DRIVE.get()))
 | 
			
		||||
            .build(d -> new DiskRecipe(d.properties(), d.ingredients()))
 | 
			
		||||
            .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "disk"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static List<TurtleItem> turtleItems() {
 | 
			
		||||
@@ -132,26 +106,22 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a crafting recipe for each turtle upgrade.
 | 
			
		||||
     *
 | 
			
		||||
     * @param add        The callback to add recipes.
 | 
			
		||||
     * @param registries The currently available registries.
 | 
			
		||||
     */
 | 
			
		||||
    private void turtleUpgrades(RecipeOutput add, HolderLookup.Provider registries) {
 | 
			
		||||
    private void turtleUpgrades() {
 | 
			
		||||
        for (var turtleItem : turtleItems()) {
 | 
			
		||||
            var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
 | 
			
		||||
 | 
			
		||||
            registries.lookupOrThrow(ITurtleUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
 | 
			
		||||
                var upgrade = upgradeHolder.value();
 | 
			
		||||
                ShapedSpecBuilder
 | 
			
		||||
                    .shaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
 | 
			
		||||
                customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
 | 
			
		||||
                    .group(name.toString())
 | 
			
		||||
                    .pattern("#T")
 | 
			
		||||
                    .define('T', turtleItem)
 | 
			
		||||
                    .define('#', upgrade.getCraftingItem().getItem())
 | 
			
		||||
                    .unlockedBy("has_items", inventoryChange(turtleItem, upgrade.getCraftingItem().getItem()))
 | 
			
		||||
                    .unlockedBy("has_items", has(turtleItem, upgrade.getCraftingItem().getItem()))
 | 
			
		||||
                    .build(ImpostorShapedRecipe::new)
 | 
			
		||||
                    .save(
 | 
			
		||||
                        add,
 | 
			
		||||
                        output,
 | 
			
		||||
                        name.withSuffix(String.format("/%s/%s", upgradeHolder.key().location().getNamespace(), upgradeHolder.key().location().getPath()))
 | 
			
		||||
                    );
 | 
			
		||||
            });
 | 
			
		||||
@@ -164,44 +134,40 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a crafting recipe for each pocket upgrade.
 | 
			
		||||
     *
 | 
			
		||||
     * @param add        The callback to add recipes.
 | 
			
		||||
     * @param registries The currently available registries.
 | 
			
		||||
     */
 | 
			
		||||
    private void pocketUpgrades(RecipeOutput add, HolderLookup.Provider registries) {
 | 
			
		||||
    private void pocketUpgrades() {
 | 
			
		||||
        for (var pocket : pocketComputerItems()) {
 | 
			
		||||
            var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, pocket).withPath(x -> x.replace("pocket_computer_", "pocket_"));
 | 
			
		||||
 | 
			
		||||
            registries.lookupOrThrow(IPocketUpgrade.REGISTRY).listElements().forEach(upgradeHolder -> {
 | 
			
		||||
                var upgrade = upgradeHolder.value();
 | 
			
		||||
                ShapedSpecBuilder
 | 
			
		||||
                    .shaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
 | 
			
		||||
                customShaped(RecipeCategory.REDSTONE, DataComponentUtil.createStack(pocket, ModRegistry.DataComponents.POCKET_UPGRADE.get(), UpgradeData.ofDefault(upgradeHolder)))
 | 
			
		||||
                    .group(name.toString())
 | 
			
		||||
                    .pattern("#")
 | 
			
		||||
                    .pattern("P")
 | 
			
		||||
                    .define('P', pocket)
 | 
			
		||||
                    .define('#', upgrade.getCraftingItem().getItem())
 | 
			
		||||
                    .unlockedBy("has_items", inventoryChange(pocket, upgrade.getCraftingItem().getItem()))
 | 
			
		||||
                    .unlockedBy("has_items", has(pocket, upgrade.getCraftingItem().getItem()))
 | 
			
		||||
                    .build(ImpostorShapedRecipe::new)
 | 
			
		||||
                    .save(
 | 
			
		||||
                        add,
 | 
			
		||||
                        output,
 | 
			
		||||
                        name.withSuffix(String.format("/%s/%s", upgradeHolder.key().location().getNamespace(), upgradeHolder.key().location().getPath()))
 | 
			
		||||
                    );
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void turtleOverlays(RecipeOutput add, HolderLookup.Provider registries) {
 | 
			
		||||
        turtleOverlay(add, registries, TurtleOverlays.TRANS_FLAG, x -> x
 | 
			
		||||
            .unlockedBy("has_dye", inventoryChange(itemPredicate(ingredients.dye())))
 | 
			
		||||
    private void turtleOverlays() {
 | 
			
		||||
        turtleOverlay(TurtleOverlays.TRANS_FLAG, x -> x
 | 
			
		||||
            .unlockedBy("has_dye", has(ingredients.dye()))
 | 
			
		||||
            .requires(ColourUtils.getDyeTag(DyeColor.LIGHT_BLUE))
 | 
			
		||||
            .requires(ColourUtils.getDyeTag(DyeColor.PINK))
 | 
			
		||||
            .requires(ColourUtils.getDyeTag(DyeColor.WHITE))
 | 
			
		||||
            .requires(Items.STICK)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        turtleOverlay(add, registries, TurtleOverlays.RAINBOW_FLAG, x -> x
 | 
			
		||||
            .unlockedBy("has_dye", inventoryChange(itemPredicate(ingredients.dye())))
 | 
			
		||||
        turtleOverlay(TurtleOverlays.RAINBOW_FLAG, x -> x
 | 
			
		||||
            .unlockedBy("has_dye", has(ingredients.dye()))
 | 
			
		||||
            .requires(ColourUtils.getDyeTag(DyeColor.RED))
 | 
			
		||||
            .requires(ColourUtils.getDyeTag(DyeColor.ORANGE))
 | 
			
		||||
            .requires(ColourUtils.getDyeTag(DyeColor.YELLOW))
 | 
			
		||||
@@ -212,264 +178,234 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void turtleOverlay(RecipeOutput add, HolderLookup.Provider registries, ResourceKey<TurtleOverlay> overlay, Consumer<ShapelessSpecBuilder> build) {
 | 
			
		||||
    private void turtleOverlay(ResourceKey<TurtleOverlay> overlay, Consumer<ShapelessSpecBuilder> build) {
 | 
			
		||||
        var holder = registries.lookupOrThrow(overlay.registryKey()).getOrThrow(overlay);
 | 
			
		||||
 | 
			
		||||
        for (var turtleItem : turtleItems()) {
 | 
			
		||||
            var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
 | 
			
		||||
 | 
			
		||||
            var builder = ShapelessSpecBuilder
 | 
			
		||||
                .shapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), holder))
 | 
			
		||||
            var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), holder))
 | 
			
		||||
                .group(name.withSuffix("_overlay").toString())
 | 
			
		||||
                .unlockedBy("has_turtle", inventoryChange(turtleItem));
 | 
			
		||||
                .unlockedBy("has_turtle", has(turtleItem));
 | 
			
		||||
            build.accept(builder);
 | 
			
		||||
            builder
 | 
			
		||||
                .requires(turtleItem)
 | 
			
		||||
                .build(s -> new TransformShapelessRecipe(s, List.of(
 | 
			
		||||
                    CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
 | 
			
		||||
                )))
 | 
			
		||||
                .save(add, name.withSuffix("_overlays/" + overlay.location().getPath()));
 | 
			
		||||
                .save(output, name.withSuffix("_overlays/" + overlay.location().getPath()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private void basicRecipes(RecipeOutput add) {
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.CABLE.get(), 6)
 | 
			
		||||
    private void basicRecipes() {
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.CABLE.get(), 6)
 | 
			
		||||
            .pattern(" # ")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern(" # ")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_modem", has(WIRED_MODEM))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_NORMAL.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_NORMAL.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_redstone", inventoryChange(itemPredicate(ingredients.redstone())))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_redstone", has(ingredients.redstone()))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_components", inventoryTrigger(itemPredicate(ingredients.redstone()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedSpecBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
        customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#C#")
 | 
			
		||||
            .pattern("# #")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
 | 
			
		||||
            .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade"));
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_COMMAND.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.COMPUTER_COMMAND.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('R', Items.COMMAND_BLOCK)
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(Items.COMMAND_BLOCK))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_components", has(Items.COMMAND_BLOCK))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedSpecBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_NORMAL.get())
 | 
			
		||||
        customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_NORMAL.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#C#")
 | 
			
		||||
            .pattern("#I#")
 | 
			
		||||
            .define('#', ingredients.ironIngot())
 | 
			
		||||
            .define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
 | 
			
		||||
            .define('I', ingredients.woodenChest())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
 | 
			
		||||
            .unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_NORMAL.get()))
 | 
			
		||||
            .build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_NORMAL.get()))))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedSpecBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
 | 
			
		||||
        customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#C#")
 | 
			
		||||
            .pattern("#I#")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
            .define('I', ingredients.woodenChest())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
 | 
			
		||||
            .unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_NORMAL.get()))
 | 
			
		||||
            .build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.COMPUTER_ADVANCED.get()))))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedSpecBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
 | 
			
		||||
        customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.TURTLE_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#C#")
 | 
			
		||||
            .pattern(" B ")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('C', ModRegistry.Items.TURTLE_NORMAL.get())
 | 
			
		||||
            .define('B', ingredients.goldBlock())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.TURTLE_NORMAL.get()))))
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
 | 
			
		||||
            .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade"));
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.DISK_DRIVE.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.DISK_DRIVE.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_NORMAL.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_NORMAL.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_ADVANCED.get(), 4)
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.MONITOR_ADVANCED.get(), 4)
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#A#")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('A', Items.GOLDEN_APPLE)
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_apple", has(Items.GOLDEN_APPLE))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#A#")
 | 
			
		||||
            .pattern("#G#")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('A', Items.GOLDEN_APPLE)
 | 
			
		||||
            .define('G', ingredients.glassPane())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_apple", inventoryChange(Items.GOLDEN_APPLE))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_apple", has(Items.GOLDEN_APPLE))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedSpecBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
 | 
			
		||||
        customShaped(RecipeCategory.REDSTONE, ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#C#")
 | 
			
		||||
            .pattern("# #")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
 | 
			
		||||
            .unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .unlockedBy("has_components", inventoryTrigger(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
 | 
			
		||||
            .build(x -> new TransformShapedRecipe(x, List.of(new CopyComponents(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()))))
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
 | 
			
		||||
            .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade"));
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTER.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTER.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern("#D#")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .define('D', ingredients.dye())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.SPEAKER.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.SPEAKER.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#N#")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('N', Items.NOTE_BLOCK)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#R#")
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_cable", inventoryChange(ModRegistry.Items.CABLE.get()))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_cable", has(ModRegistry.Items.CABLE.get()))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapelessRecipeBuilder
 | 
			
		||||
            .shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM_FULL.get())
 | 
			
		||||
            .requires(ModRegistry.Items.WIRED_MODEM.get())
 | 
			
		||||
            .unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "wired_modem_full_from"));
 | 
			
		||||
        ShapelessRecipeBuilder
 | 
			
		||||
            .shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.WIRED_MODEM.get())
 | 
			
		||||
            .requires(ModRegistry.Items.WIRED_MODEM_FULL.get())
 | 
			
		||||
            .unlockedBy("has_modem", inventoryChange(WIRED_MODEM))
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "wired_modem_full_to"));
 | 
			
		||||
        oneToOneConversionRecipe(ModRegistry.Items.WIRED_MODEM.get(), ModRegistry.Items.WIRED_MODEM_FULL.get(), null);
 | 
			
		||||
        oneToOneConversionRecipe(ModRegistry.Items.WIRED_MODEM_FULL.get(), ModRegistry.Items.WIRED_MODEM.get(), null);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_NORMAL.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_NORMAL.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#E#")
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .define('#', Items.STONE)
 | 
			
		||||
            .define('E', ingredients.enderPearl())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get())
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .pattern("#E#")
 | 
			
		||||
            .pattern("###")
 | 
			
		||||
            .define('#', ingredients.goldIngot())
 | 
			
		||||
            .define('E', Items.ENDER_EYE)
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_wireless", inventoryChange(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_computer", has(COMPUTER))
 | 
			
		||||
            .unlockedBy("has_wireless", has(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()))
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapelessSpecBuilder
 | 
			
		||||
            .shapeless(RecipeCategory.DECORATIONS, playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c"))
 | 
			
		||||
        customShapeless(RecipeCategory.DECORATIONS, playerHead("Cloudhunter", "6d074736-b1e9-4378-a99b-bd8777821c9c"))
 | 
			
		||||
            .requires(ItemTags.SKULLS)
 | 
			
		||||
            .requires(ModRegistry.Items.MONITOR_NORMAL.get())
 | 
			
		||||
            .unlockedBy("has_monitor", inventoryChange(ModRegistry.Items.MONITOR_NORMAL.get()))
 | 
			
		||||
            .unlockedBy("has_monitor", has(ModRegistry.Items.MONITOR_NORMAL.get()))
 | 
			
		||||
            .build()
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_cloudy"));
 | 
			
		||||
            .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_cloudy"));
 | 
			
		||||
 | 
			
		||||
        ShapelessSpecBuilder
 | 
			
		||||
            .shapeless(RecipeCategory.DECORATIONS, playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb"))
 | 
			
		||||
        customShapeless(RecipeCategory.DECORATIONS, playerHead("dan200", "f3c8d69b-0776-4512-8434-d1b2165909eb"))
 | 
			
		||||
            .requires(ItemTags.SKULLS)
 | 
			
		||||
            .requires(ModRegistry.Items.COMPUTER_ADVANCED.get())
 | 
			
		||||
            .unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_ADVANCED.get()))
 | 
			
		||||
            .unlockedBy("has_computer", has(ModRegistry.Items.COMPUTER_ADVANCED.get()))
 | 
			
		||||
            .build()
 | 
			
		||||
            .save(add, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_dan200"));
 | 
			
		||||
            .save(output, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "skull_dan200"));
 | 
			
		||||
 | 
			
		||||
        var pages = Ingredient.of(
 | 
			
		||||
            ModRegistry.Items.PRINTED_PAGE.get(),
 | 
			
		||||
@@ -477,76 +413,84 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
 | 
			
		||||
            Items.PAPER
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        ShapelessSpecBuilder
 | 
			
		||||
            .shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get())
 | 
			
		||||
        customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_PAGES.get())
 | 
			
		||||
            .requires(ingredients.string())
 | 
			
		||||
            .unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get()))
 | 
			
		||||
            .unlockedBy("has_printer", has(ModRegistry.Items.PRINTER.get()))
 | 
			
		||||
            .build(x -> new PrintoutRecipe(x, pages, 2))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapelessSpecBuilder
 | 
			
		||||
            .shapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get())
 | 
			
		||||
        customShapeless(RecipeCategory.REDSTONE, ModRegistry.Items.PRINTED_BOOK.get())
 | 
			
		||||
            .requires(ingredients.leather())
 | 
			
		||||
            .requires(ingredients.string())
 | 
			
		||||
            .unlockedBy("has_printer", inventoryChange(ModRegistry.Items.PRINTER.get()))
 | 
			
		||||
            .unlockedBy("has_printer", has(ModRegistry.Items.PRINTER.get()))
 | 
			
		||||
            .build(x -> new PrintoutRecipe(x, pages, 1))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .save(output);
 | 
			
		||||
 | 
			
		||||
        ShapedRecipeBuilder
 | 
			
		||||
            .shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.REDSTONE_RELAY.get())
 | 
			
		||||
        shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.REDSTONE_RELAY.get())
 | 
			
		||||
            .pattern("SRS")
 | 
			
		||||
            .pattern("RCR")
 | 
			
		||||
            .pattern("SRS")
 | 
			
		||||
            .define('S', Items.STONE)
 | 
			
		||||
            .define('R', ingredients.redstone())
 | 
			
		||||
            .define('C', ModRegistry.Blocks.CABLE.get())
 | 
			
		||||
            .unlockedBy("has_cable", inventoryChange(ModRegistry.Blocks.CABLE.get()))
 | 
			
		||||
            .save(add);
 | 
			
		||||
            .unlockedBy("has_cable", has(ModRegistry.Blocks.CABLE.get()))
 | 
			
		||||
            .save(output);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static DyeColor ofColour(Colour colour) {
 | 
			
		||||
        return DyeColor.byId(15 - colour.ordinal());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Criterion<InventoryChangeTrigger.TriggerInstance> inventoryChange(TagKey<Item> stack) {
 | 
			
		||||
        return InventoryChangeTrigger.TriggerInstance.hasItems(itemPredicate(stack));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Criterion<InventoryChangeTrigger.TriggerInstance> inventoryChange(ItemLike... stack) {
 | 
			
		||||
        return InventoryChangeTrigger.TriggerInstance.hasItems(stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Criterion<InventoryChangeTrigger.TriggerInstance> inventoryChange(ItemPredicate... items) {
 | 
			
		||||
    private static Criterion<InventoryChangeTrigger.TriggerInstance> has(ItemLike... items) {
 | 
			
		||||
        return InventoryChangeTrigger.TriggerInstance.hasItems(items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ItemPredicate itemPredicate(ItemLike item) {
 | 
			
		||||
        return ItemPredicate.Builder.item().of(item).build();
 | 
			
		||||
    private ItemPredicate itemPredicate(ItemLike item) {
 | 
			
		||||
        return ItemPredicate.Builder.item().of(items, item).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ItemPredicate itemPredicate(TagKey<Item> item) {
 | 
			
		||||
        return ItemPredicate.Builder.item().of(item).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ItemPredicate itemPredicate(Ingredient ingredient) {
 | 
			
		||||
        var json = Ingredient.CODEC_NONEMPTY.encodeStart(JsonOps.INSTANCE, ingredient).getOrThrow();
 | 
			
		||||
        if (!(json instanceof JsonObject object)) throw new IllegalStateException("Unknown ingredient " + json);
 | 
			
		||||
 | 
			
		||||
        if (object.has("item")) {
 | 
			
		||||
            var item = ItemStack.SIMPLE_ITEM_CODEC.parse(JsonOps.INSTANCE, object).getOrThrow();
 | 
			
		||||
            return itemPredicate(item.getItem());
 | 
			
		||||
        } else if (object.has("tag")) {
 | 
			
		||||
            return itemPredicate(TagKey.create(Registries.ITEM, ResourceLocation.parse(GsonHelper.getAsString(object, "tag"))));
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new IllegalArgumentException("Unknown ingredient " + json);
 | 
			
		||||
        }
 | 
			
		||||
    private ItemPredicate itemPredicate(TagKey<Item> item) {
 | 
			
		||||
        return ItemPredicate.Builder.item().of(items, item).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ItemStack playerHead(String name, String uuid) {
 | 
			
		||||
        return DataComponentUtil.createStack(Items.PLAYER_HEAD, DataComponents.PROFILE, new ResolvableProfile(new GameProfile(UUID.fromString(uuid), name)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void addSpecial(RecipeOutput add, Recipe<?> recipe) {
 | 
			
		||||
        add.accept(RegistryHelper.getKeyOrThrow(BuiltInRegistries.RECIPE_SERIALIZER, recipe.getSerializer()), recipe, null);
 | 
			
		||||
    private ShapedSpecBuilder customShaped(RecipeCategory category, ItemStack result) {
 | 
			
		||||
        return new ShapedSpecBuilder(items, category, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ShapedSpecBuilder customShaped(RecipeCategory category, ItemLike result) {
 | 
			
		||||
        return new ShapedSpecBuilder(items, category, new ItemStack(result));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ShapelessSpecBuilder customShapeless(RecipeCategory category, ItemStack result) {
 | 
			
		||||
        return new ShapelessSpecBuilder(items, category, result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private ShapelessSpecBuilder customShapeless(RecipeCategory category, ItemLike result) {
 | 
			
		||||
        return new ShapelessSpecBuilder(items, category, new ItemStack(result));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void special(Recipe<?> recipe) {
 | 
			
		||||
        var key = RegistryHelper.getKeyOrThrow(BuiltInRegistries.RECIPE_SERIALIZER, recipe.getSerializer());
 | 
			
		||||
        output.accept(recipeKey(key), recipe, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ResourceKey<Recipe<?>> recipeKey(ResourceLocation key) {
 | 
			
		||||
        return ResourceKey.create(Registries.RECIPE, key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static class Runner extends net.minecraft.data.recipes.RecipeProvider.Runner {
 | 
			
		||||
        protected Runner(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
 | 
			
		||||
            super(output, registries);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        protected RecipeProvider createRecipeProvider(HolderLookup.Provider registries, RecipeOutput output) {
 | 
			
		||||
            return new RecipeProvider(registries, output);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public String getName() {
 | 
			
		||||
            return "Recipes";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,14 @@
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data;
 | 
			
		||||
package dan200.computercraft.data.client;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.client.item.model.TurtleOverlayModel;
 | 
			
		||||
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
 | 
			
		||||
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
 | 
			
		||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
 | 
			
		||||
@@ -16,13 +20,18 @@ import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
 | 
			
		||||
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
 | 
			
		||||
import dan200.computercraft.shared.util.DirectionUtil;
 | 
			
		||||
import net.minecraft.client.data.models.BlockModelGenerators;
 | 
			
		||||
import net.minecraft.client.data.models.blockstates.*;
 | 
			
		||||
import net.minecraft.client.data.models.model.*;
 | 
			
		||||
import net.minecraft.client.renderer.item.EmptyModel;
 | 
			
		||||
import net.minecraft.client.renderer.item.properties.conditional.HasComponent;
 | 
			
		||||
import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.data.models.BlockModelGenerators;
 | 
			
		||||
import net.minecraft.data.models.blockstates.*;
 | 
			
		||||
import net.minecraft.data.models.model.*;
 | 
			
		||||
import net.minecraft.core.component.DataComponents;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.level.block.Block;
 | 
			
		||||
import net.minecraft.world.level.block.Blocks;
 | 
			
		||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
 | 
			
		||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
 | 
			
		||||
@@ -34,10 +43,10 @@ import java.util.Optional;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocation;
 | 
			
		||||
import static net.minecraft.data.models.model.TextureMapping.getBlockTexture;
 | 
			
		||||
import static net.minecraft.client.data.models.model.ModelLocationUtils.getModelLocation;
 | 
			
		||||
import static net.minecraft.client.data.models.model.TextureMapping.getBlockTexture;
 | 
			
		||||
 | 
			
		||||
class BlockModelProvider {
 | 
			
		||||
public class BlockModelProvider {
 | 
			
		||||
    private static final TextureSlot CURSOR = TextureSlot.create("cursor");
 | 
			
		||||
    private static final TextureSlot LEFT = TextureSlot.create("left");
 | 
			
		||||
    private static final TextureSlot RIGHT = TextureSlot.create("right");
 | 
			
		||||
@@ -92,6 +101,8 @@ class BlockModelProvider {
 | 
			
		||||
        registerMonitor(generators, ModRegistry.Blocks.MONITOR_ADVANCED.get());
 | 
			
		||||
 | 
			
		||||
        generators.createHorizontallyRotatedBlock(ModRegistry.Blocks.SPEAKER.get(), TexturedModel.ORIENTABLE_ONLY_TOP);
 | 
			
		||||
        registerSimpleItemModel(generators, ModRegistry.Blocks.SPEAKER.get());
 | 
			
		||||
 | 
			
		||||
        registerDiskDrive(generators);
 | 
			
		||||
        registerPrinter(generators);
 | 
			
		||||
 | 
			
		||||
@@ -104,10 +115,10 @@ class BlockModelProvider {
 | 
			
		||||
        registerTurtleModem(generators, "block/turtle_modem_normal", "block/wireless_modem_normal_face");
 | 
			
		||||
        registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
 | 
			
		||||
 | 
			
		||||
        generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(
 | 
			
		||||
            ModRegistry.Blocks.LECTERN.get(),
 | 
			
		||||
            Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(Blocks.LECTERN))
 | 
			
		||||
        ).with(createHorizontalFacingDispatch()));
 | 
			
		||||
        generators.blockStateOutput.accept(
 | 
			
		||||
            BlockModelGenerators.createSimpleBlock(ModRegistry.Blocks.LECTERN.get(), getModelLocation(Blocks.LECTERN))
 | 
			
		||||
                .with(createHorizontalFacingDispatch())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerDiskDrive(BlockModelGenerators generators) {
 | 
			
		||||
@@ -127,7 +138,7 @@ class BlockModelProvider {
 | 
			
		||||
                );
 | 
			
		||||
            }))
 | 
			
		||||
        );
 | 
			
		||||
        generators.delegateItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
 | 
			
		||||
        generators.registerSimpleItemModel(diskDrive, getModelLocation(diskDrive, "_empty"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerPrinter(BlockModelGenerators generators) {
 | 
			
		||||
@@ -155,7 +166,7 @@ class BlockModelProvider {
 | 
			
		||||
                );
 | 
			
		||||
            }))
 | 
			
		||||
        );
 | 
			
		||||
        generators.delegateItemModel(printer, getModelLocation(printer, "_empty"));
 | 
			
		||||
        generators.registerSimpleItemModel(printer, getModelLocation(printer, "_empty"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerComputer(BlockModelGenerators generators, ComputerBlock<?> block) {
 | 
			
		||||
@@ -174,10 +185,17 @@ class BlockModelProvider {
 | 
			
		||||
                );
 | 
			
		||||
            }))
 | 
			
		||||
        );
 | 
			
		||||
        generators.delegateItemModel(block, getModelLocation(block, "_blinking"));
 | 
			
		||||
        generators.registerSimpleItemModel(block, getModelLocation(block, "_blinking"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerTurtle(BlockModelGenerators generators, TurtleBlock block) {
 | 
			
		||||
        // The actual turtle blockstate is an empty model with just the partcles.
 | 
			
		||||
        var particleModel = ModelTemplates.PARTICLE_ONLY.createWithSuffix(
 | 
			
		||||
            block, "_particle", TextureMapping.particle(getBlockTexture(block, "_front")), generators.modelOutput
 | 
			
		||||
        );
 | 
			
		||||
        generators.blockStateOutput.accept(BlockModelGenerators.createSimpleBlock(block, particleModel));
 | 
			
		||||
 | 
			
		||||
        // We then register the full model for use in items and the BE renderer.
 | 
			
		||||
        var model = TURTLE.create(block, new TextureMapping()
 | 
			
		||||
                .put(TextureSlot.FRONT, getBlockTexture(block, "_front"))
 | 
			
		||||
                .put(TextureSlot.BACK, getBlockTexture(block, "_back"))
 | 
			
		||||
@@ -188,17 +206,21 @@ class BlockModelProvider {
 | 
			
		||||
                .put(BACKPACK, getBlockTexture(block, "_backpack")),
 | 
			
		||||
            generators.modelOutput
 | 
			
		||||
        );
 | 
			
		||||
        generators.blockStateOutput.accept(
 | 
			
		||||
            MultiVariantGenerator.multiVariant(block, Variant.variant().with(VariantProperties.MODEL, model))
 | 
			
		||||
                .with(createHorizontalFacingDispatch())
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        generators.modelOutput.accept(getModelLocation(block.asItem()), () -> {
 | 
			
		||||
            var out = new JsonObject();
 | 
			
		||||
            out.addProperty("loader", "computercraft:turtle");
 | 
			
		||||
            out.addProperty("model", model.toString());
 | 
			
		||||
            return out;
 | 
			
		||||
        });
 | 
			
		||||
        generators.itemModelOutput.accept(block.asItem(), ItemModelUtils.composite(
 | 
			
		||||
            ItemModelUtils.conditional(
 | 
			
		||||
                new HasComponent(DataComponents.DYED_COLOR, false),
 | 
			
		||||
                ItemModelUtils.plainModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL),
 | 
			
		||||
                ItemModelUtils.plainModel(model)
 | 
			
		||||
            ),
 | 
			
		||||
            new TurtleUpgradeModel.Unbaked(TurtleSide.LEFT, model),
 | 
			
		||||
            new TurtleUpgradeModel.Unbaked(TurtleSide.RIGHT, model),
 | 
			
		||||
            new TurtleOverlayModel.Unbaked(model),
 | 
			
		||||
            ItemModelUtils.isXmas(
 | 
			
		||||
                ItemModelUtils.conditional(TurtleShowElfOverlay.create(), ItemModelUtils.plainModel(TurtleOverlay.ELF_MODEL), new EmptyModel.Unbaked()),
 | 
			
		||||
                new EmptyModel.Unbaked()
 | 
			
		||||
            )
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerWirelessModem(BlockModelGenerators generators, WirelessModemBlock block) {
 | 
			
		||||
@@ -207,7 +229,7 @@ class BlockModelProvider {
 | 
			
		||||
            .with(createModelDispatch(WirelessModemBlock.ON,
 | 
			
		||||
                on -> modemModel(generators, getModelLocation(block, on ? "_on" : "_off"), getBlockTexture(block, "_face" + (on ? "_on" : "")))
 | 
			
		||||
            )));
 | 
			
		||||
        generators.delegateItemModel(block, getModelLocation(block, "_off"));
 | 
			
		||||
        generators.registerSimpleItemModel(block, getModelLocation(block, "_off"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerWiredModems(BlockModelGenerators generators) {
 | 
			
		||||
@@ -227,8 +249,8 @@ class BlockModelProvider {
 | 
			
		||||
                );
 | 
			
		||||
            })));
 | 
			
		||||
 | 
			
		||||
        generators.delegateItemModel(fullBlock, getModelLocation(fullBlock, "_off"));
 | 
			
		||||
        generators.delegateItemModel(ModRegistry.Items.WIRED_MODEM.get(), ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_off"));
 | 
			
		||||
        generators.registerSimpleItemModel(fullBlock, getModelLocation(fullBlock, "_off"));
 | 
			
		||||
        generators.registerSimpleItemModel(ModRegistry.Items.WIRED_MODEM.get(), ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/wired_modem_off"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ResourceLocation modemModel(BlockModelGenerators generators, ResourceLocation name, ResourceLocation texture) {
 | 
			
		||||
@@ -264,7 +286,7 @@ class BlockModelProvider {
 | 
			
		||||
            .with(createVerticalFacingDispatch(MonitorBlock.ORIENTATION))
 | 
			
		||||
            .with(createModelDispatch(MonitorBlock.STATE, edge -> getModelLocation(block, edge == MonitorEdgeState.NONE ? "" : "_" + edge.getSerializedName())))
 | 
			
		||||
        );
 | 
			
		||||
        generators.delegateItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
 | 
			
		||||
        generators.registerSimpleItemModel(block, monitorModel(generators, block, "_item", 15, 4, 0, 32));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ResourceLocation monitorModel(BlockModelGenerators generators, MonitorBlock block, String corners, int front, int side, int top, int back) {
 | 
			
		||||
@@ -355,17 +377,13 @@ class BlockModelProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        generators.blockStateOutput.accept(generator);
 | 
			
		||||
        generators.registerSimpleItemModel(ModRegistry.Items.CABLE.get(), getModelLocation(ModRegistry.Items.CABLE.get()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerRedstoneControl(BlockModelGenerators generators) {
 | 
			
		||||
        var redstoneControl = ModRegistry.Blocks.REDSTONE_RELAY.get();
 | 
			
		||||
        var model = ModelTemplates.CUBE_ORIENTABLE_TOP_BOTTOM.create(
 | 
			
		||||
            redstoneControl, TextureMapping.orientableCube(redstoneControl), generators.modelOutput
 | 
			
		||||
        );
 | 
			
		||||
        generators.blockStateOutput.accept(
 | 
			
		||||
            MultiVariantGenerator.multiVariant(redstoneControl, Variant.variant().with(VariantProperties.MODEL, model))
 | 
			
		||||
                .with(createHorizontalFacingDispatch())
 | 
			
		||||
        );
 | 
			
		||||
        generators.createHorizontallyRotatedBlock(redstoneControl, TexturedModel.ORIENTABLE);
 | 
			
		||||
        registerSimpleItemModel(generators, redstoneControl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -396,6 +414,10 @@ class BlockModelProvider {
 | 
			
		||||
        registerTurtleUpgrade(generators, name + "_on", texture + "_on");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void registerSimpleItemModel(BlockModelGenerators generators, Block block) {
 | 
			
		||||
        generators.registerSimpleItemModel(block, ModelLocationUtils.getModelLocation(block));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static VariantProperties.Rotation toXAngle(Direction direction) {
 | 
			
		||||
        return switch (direction) {
 | 
			
		||||
            default -> VariantProperties.Rotation.R0;
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user