mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-03 23:22:59 +00:00 
			
		
		
		
	Compare commits
	
		
			52 Commits
		
	
	
		
			v1.20.5-1.
			...
			v1.21-1.11
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2c740bb904 | ||
| 
						 | 
					0e4710a956 | ||
| 
						 | 
					aca1d43550 | ||
| 
						 | 
					f10e401aea | ||
| 
						 | 
					7744d2663b | ||
| 
						 | 
					4566cb8273 | ||
| 
						 | 
					052e7a7ae5 | ||
| 
						 | 
					0895200681 | ||
| 
						 | 
					1a1623075f | ||
| 
						 | 
					54a95e07a4 | ||
| 
						 | 
					09d0f563b7 | ||
| 
						 | 
					e188f1d3fa | ||
| 
						 | 
					efd9a0f315 | ||
| 
						 | 
					28f75a0687 | ||
| 
						 | 
					819a4f7231 | ||
| 
						 | 
					898cb2a95d | ||
| 
						 | 
					03a8f83191 | ||
| 
						 | 
					aef92c8ebc | ||
| 
						 | 
					571ea794a8 | ||
| 
						 | 
					4b102f16b3 | ||
| 
						 | 
					e81af93043 | ||
| 
						 | 
					bb933d0100 | ||
| 
						 | 
					25b8a65c5c | ||
| 
						 | 
					e4236824d7 | ||
| 
						 | 
					cfd11ffa92 | ||
| 
						 | 
					ce133a5e66 | ||
| 
						 | 
					038fbc1ed1 | ||
| 
						 | 
					c582fb521c | ||
| 
						 | 
					af21792844 | ||
| 
						 | 
					9fbb1070ef | ||
| 
						 | 
					1944995c33 | ||
| 
						 | 
					ac851a795b | ||
| 
						 | 
					334761788a | ||
| 
						 | 
					5af3e15dd5 | ||
| 
						 | 
					de078e3037 | ||
| 
						 | 
					209b1ddbf9 | ||
| 
						 | 
					0c9f9a8652 | ||
| 
						 | 
					862d92785e | ||
| 
						 | 
					d48b85d50c | ||
| 
						 | 
					4d619de357 | ||
| 
						 | 
					57c289f173 | ||
| 
						 | 
					f63f85921f | ||
| 
						 | 
					c7e49d1929 | ||
| 
						 | 
					eb584aa94d | ||
| 
						 | 
					ad70e2ad90 | ||
| 
						 | 
					d9b0cc7075 | ||
| 
						 | 
					2c0d8263d3 | ||
| 
						 | 
					1e214f329e | ||
| 
						 | 
					de930c8d09 | ||
| 
						 | 
					94c864759d | ||
| 
						 | 
					735e7ce09b | ||
| 
						 | 
					6e9799316a | 
							
								
								
									
										6
									
								
								.github/ISSUE_TEMPLATE/bug_report.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/ISSUE_TEMPLATE/bug_report.yaml
									
									
									
									
										vendored
									
									
								
							@@ -8,10 +8,8 @@ body:
 | 
			
		||||
    label: Minecraft Version
 | 
			
		||||
    description: What version of Minecraft are you using?
 | 
			
		||||
    options:
 | 
			
		||||
      - 1.16.x
 | 
			
		||||
      - 1.18.x
 | 
			
		||||
      - 1.19.x
 | 
			
		||||
      - 1.20.x
 | 
			
		||||
      - 1.20.1
 | 
			
		||||
      - 1.21.x
 | 
			
		||||
  validations:
 | 
			
		||||
    required: true
 | 
			
		||||
- type: input
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								.github/workflows/make-doc.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/make-doc.sh
									
									
									
									
										vendored
									
									
								
							@@ -1,19 +0,0 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
set -eu
 | 
			
		||||
 | 
			
		||||
DEST="${GITHUB_REF#refs/*/}"
 | 
			
		||||
echo "Uploading docs to https://tweaked.cc/$DEST"
 | 
			
		||||
 | 
			
		||||
# Setup ssh key
 | 
			
		||||
mkdir -p "$HOME/.ssh/"
 | 
			
		||||
echo "$SSH_KEY" > "$HOME/.ssh/key"
 | 
			
		||||
chmod 600 "$HOME/.ssh/key"
 | 
			
		||||
 | 
			
		||||
# And upload
 | 
			
		||||
rsync -avc -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no -p $SSH_PORT" \
 | 
			
		||||
      "$GITHUB_WORKSPACE/projects/web/build/site/" \
 | 
			
		||||
      "$SSH_USER@$SSH_HOST:/$DEST"
 | 
			
		||||
rsync -avc -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no -p $SSH_PORT" \
 | 
			
		||||
      "$GITHUB_WORKSPACE/projects/common-api/build/docs/javadoc/" \
 | 
			
		||||
      "$SSH_USER@$SSH_HOST:/$DEST/javadoc"
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/make-doc.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/make-doc.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,8 +3,7 @@ name: Build documentation
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
    - mc-1.19.x
 | 
			
		||||
    - mc-1.20.x
 | 
			
		||||
    - mc-*
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  make_doc:
 | 
			
		||||
@@ -12,30 +11,25 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Clone repository
 | 
			
		||||
    - name: 📥 Clone repository
 | 
			
		||||
      uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
    - name: Set up Java
 | 
			
		||||
    - name: 📥 Set up Java
 | 
			
		||||
      uses: actions/setup-java@v4
 | 
			
		||||
      with:
 | 
			
		||||
        java-version: 21
 | 
			
		||||
        distribution: 'temurin'
 | 
			
		||||
 | 
			
		||||
    - name: Setup Gradle
 | 
			
		||||
    - name: 📥 Setup Gradle
 | 
			
		||||
      uses: gradle/actions/setup-gradle@v3
 | 
			
		||||
      with:
 | 
			
		||||
        cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}
 | 
			
		||||
 | 
			
		||||
    - name: Build with Gradle
 | 
			
		||||
      run: ./gradlew compileJava --no-daemon || ./gradlew compileJava --no-daemon
 | 
			
		||||
    - name: ⚒️ Generate documentation
 | 
			
		||||
      run: ./gradlew docWebsite --no-daemon
 | 
			
		||||
 | 
			
		||||
    - name: Generate documentation
 | 
			
		||||
      run: ./gradlew docWebsite :common-api:javadoc  --no-daemon
 | 
			
		||||
 | 
			
		||||
    - name: Upload documentation
 | 
			
		||||
      run: .github/workflows/make-doc.sh 2> /dev/null
 | 
			
		||||
      env:
 | 
			
		||||
        SSH_KEY:  ${{ secrets.SSH_KEY  }}
 | 
			
		||||
        SSH_USER: ${{ secrets.SSH_USER }}
 | 
			
		||||
        SSH_HOST: ${{ secrets.SSH_HOST }}
 | 
			
		||||
        SSH_PORT: ${{ secrets.SSH_PORT }}
 | 
			
		||||
    - name: 📤 Upload Jar
 | 
			
		||||
      uses: actions/upload-artifact@v4
 | 
			
		||||
      with:
 | 
			
		||||
        name: Documentation
 | 
			
		||||
        path: ./projects/web/build/site/
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ on is present.
 | 
			
		||||
```groovy
 | 
			
		||||
repositories {
 | 
			
		||||
  maven {
 | 
			
		||||
    url "https://squiddev.cc/maven/"
 | 
			
		||||
    url "https://maven.squiddev.cc"
 | 
			
		||||
    content {
 | 
			
		||||
      includeGroup("cc.tweaked")
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ repositories {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    maven("https://squiddev.cc/maven") {
 | 
			
		||||
    maven("https://maven.squiddev.cc") {
 | 
			
		||||
        name = "SquidDev"
 | 
			
		||||
        content {
 | 
			
		||||
            includeGroup("cc.tweaked.vanilla-extract")
 | 
			
		||||
 
 | 
			
		||||
@@ -30,11 +30,6 @@ subsystems {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
 | 
			
		||||
    implementation("net.neoforged:neoforge:${libs.findVersion("neoForge").get()}")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MinecraftConfigurations.setup(project)
 | 
			
		||||
 | 
			
		||||
extensions.configure(CCTweakedExtension::class.java) {
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ java {
 | 
			
		||||
repositories {
 | 
			
		||||
    mavenCentral()
 | 
			
		||||
 | 
			
		||||
    val mainMaven = maven("https://squiddev.cc/maven") {
 | 
			
		||||
    val mainMaven = maven("https://maven.squiddev.cc/mirror") {
 | 
			
		||||
        name = "SquidDev"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +56,6 @@ repositories {
 | 
			
		||||
            includeGroup("mezz.jei")
 | 
			
		||||
            includeGroup("org.teavm")
 | 
			
		||||
            includeModule("com.terraformersmc", "modmenu")
 | 
			
		||||
            includeModule("me.lucko", "fabric-permissions-api")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -95,9 +94,8 @@ sourceSets.all {
 | 
			
		||||
            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)
 | 
			
		||||
            check("UnusedVariable", CheckSeverity.OFF) // Too many false positives with records.
 | 
			
		||||
            check("EnumOrdinal", CheckSeverity.OFF) // For now. We could replace most of these with EnumMap.
 | 
			
		||||
            check("OperatorPrecedence", CheckSeverity.OFF) // For now.
 | 
			
		||||
            check("AlreadyChecked", CheckSeverity.OFF) // Seems to be broken?
 | 
			
		||||
            check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid
 | 
			
		||||
            check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
 | 
			
		||||
 | 
			
		||||
@@ -135,8 +133,8 @@ tasks.processResources {
 | 
			
		||||
tasks.withType(AbstractArchiveTask::class.java).configureEach {
 | 
			
		||||
    isPreserveFileTimestamps = false
 | 
			
		||||
    isReproducibleFileOrder = true
 | 
			
		||||
    dirMode = Integer.valueOf("755", 8)
 | 
			
		||||
    fileMode = Integer.valueOf("664", 8)
 | 
			
		||||
    filePermissions {}
 | 
			
		||||
    dirPermissions {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.jar {
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ publishing {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    repositories {
 | 
			
		||||
        maven("https://squiddev.cc/maven") {
 | 
			
		||||
        maven("https://maven.squiddev.cc") {
 | 
			
		||||
            name = "SquidDev"
 | 
			
		||||
 | 
			
		||||
            credentials(PasswordCredentials::class)
 | 
			
		||||
 
 | 
			
		||||
@@ -225,12 +225,12 @@ abstract class CCTweakedExtension(
 | 
			
		||||
     * where possible.
 | 
			
		||||
     */
 | 
			
		||||
    fun downloadFile(label: String, url: String): File {
 | 
			
		||||
        val url = URI(url)
 | 
			
		||||
        val path = File(url.path)
 | 
			
		||||
        val uri = URI(url)
 | 
			
		||||
        val path = File(uri.path)
 | 
			
		||||
 | 
			
		||||
        project.repositories.ivy {
 | 
			
		||||
            name = label
 | 
			
		||||
            setUrl(URI(url.scheme, url.userInfo, url.host, url.port, path.parent, null, null))
 | 
			
		||||
            setUrl(URI(uri.scheme, uri.userInfo, uri.host, uri.port, path.parent, null, null))
 | 
			
		||||
            patternLayout {
 | 
			
		||||
                artifact("[artifact].[ext]")
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import org.gradle.api.file.FileSystemLocation
 | 
			
		||||
import org.gradle.api.provider.Property
 | 
			
		||||
import org.gradle.api.provider.Provider
 | 
			
		||||
import org.gradle.api.tasks.JavaExec
 | 
			
		||||
import org.gradle.kotlin.dsl.getByName
 | 
			
		||||
import org.gradle.process.BaseExecSpec
 | 
			
		||||
import org.gradle.process.JavaExecSpec
 | 
			
		||||
import org.gradle.process.ProcessForkOptions
 | 
			
		||||
@@ -46,6 +47,21 @@ fun JavaExec.copyToFull(spec: JavaExec) {
 | 
			
		||||
    copyToExec(spec)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base this [JavaExec] task on an existing task.
 | 
			
		||||
 */
 | 
			
		||||
fun JavaExec.copyFromTask(task: JavaExec) {
 | 
			
		||||
    for (dep in task.dependsOn) dependsOn(dep)
 | 
			
		||||
    task.copyToFull(this)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base this [JavaExec] task on an existing task.
 | 
			
		||||
 */
 | 
			
		||||
fun JavaExec.copyFromTask(task: String) {
 | 
			
		||||
    copyFromTask(project.tasks.getByName<JavaExec>(task))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Copy additional [BaseExecSpec] options which aren't handled by [ProcessForkOptions.copyTo].
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.gradle
 | 
			
		||||
 | 
			
		||||
import net.neoforged.gradle.common.runs.run.RunImpl
 | 
			
		||||
import net.neoforged.gradle.common.runs.tasks.RunExec
 | 
			
		||||
import net.neoforged.gradle.dsl.common.extensions.RunnableSourceSet
 | 
			
		||||
import net.neoforged.gradle.dsl.common.runs.run.Run
 | 
			
		||||
import org.gradle.api.plugins.JavaPluginExtension
 | 
			
		||||
import org.gradle.api.tasks.JavaExec
 | 
			
		||||
import org.gradle.api.tasks.SourceSet
 | 
			
		||||
import org.gradle.jvm.toolchain.JavaToolchainService
 | 
			
		||||
import org.gradle.kotlin.dsl.assign
 | 
			
		||||
import org.gradle.kotlin.dsl.create
 | 
			
		||||
import org.gradle.kotlin.dsl.findByType
 | 
			
		||||
import java.nio.file.Files
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set [JavaExec] task to run a given [RunConfig].
 | 
			
		||||
 *
 | 
			
		||||
 * See also [RunExec].
 | 
			
		||||
 */
 | 
			
		||||
fun JavaExec.setRunConfig(config: Run) {
 | 
			
		||||
    mainClass.set(config.mainClass)
 | 
			
		||||
    workingDir = config.workingDirectory.get().asFile
 | 
			
		||||
    argumentProviders.add { config.programArguments.get() }
 | 
			
		||||
    jvmArgumentProviders.add { config.jvmArguments.get() }
 | 
			
		||||
 | 
			
		||||
    environment(config.environmentVariables.get())
 | 
			
		||||
    systemProperties(config.systemProperties.get())
 | 
			
		||||
 | 
			
		||||
    config.modSources.get().forEach { classpath(it.runtimeClasspath) }
 | 
			
		||||
    classpath(config.classpath)
 | 
			
		||||
    classpath(config.dependencies.get().configuration)
 | 
			
		||||
 | 
			
		||||
    (config as RunImpl).taskDependencies.forEach { dependsOn(it) }
 | 
			
		||||
 | 
			
		||||
    javaLauncher.set(
 | 
			
		||||
        project.extensions.getByType(JavaToolchainService::class.java)
 | 
			
		||||
            .launcherFor(project.extensions.getByType(JavaPluginExtension::class.java).toolchain),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    doFirst("Create working directory") { Files.createDirectories(workingDir.toPath()) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Add a new [Run.modSource] with a specific mod id.
 | 
			
		||||
 */
 | 
			
		||||
fun Run.modSourceAs(sourceSet: SourceSet, mod: String) {
 | 
			
		||||
    // NeoGradle requires a RunnableSourceSet to be present, so we inject it into other project's source sets.
 | 
			
		||||
    val runnable = sourceSet.extensions.findByType<RunnableSourceSet>() ?: run {
 | 
			
		||||
        val extension = sourceSet.extensions.create<RunnableSourceSet>(RunnableSourceSet.NAME, project)
 | 
			
		||||
        extension.modIdentifier = mod
 | 
			
		||||
        extension.modIdentifier.finalizeValueOnRead()
 | 
			
		||||
        extension
 | 
			
		||||
    }
 | 
			
		||||
    if (runnable.modIdentifier.get() != mod) throw IllegalArgumentException("Multiple mod identifiers")
 | 
			
		||||
 | 
			
		||||
    modSource(sourceSet)
 | 
			
		||||
}
 | 
			
		||||
@@ -98,7 +98,6 @@ abstract class MergeTrees : DefaultTask() {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val sharedFiles = files.entries.asSequence().filter { (_, v) -> v.found == sources.size }.map { (k, _) -> k }.toList()
 | 
			
		||||
        println(sharedFiles)
 | 
			
		||||
 | 
			
		||||
        // Copy shared files to the common directory
 | 
			
		||||
        fsOperations.sync {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								doc/index.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								doc/index.md
									
									
									
									
									
								
							@@ -4,7 +4,14 @@ SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
 | 
			
		||||
SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
# 
 | 
			
		||||
<h1>
 | 
			
		||||
    <picture>
 | 
			
		||||
        <source media="(prefers-color-scheme: dark)" srcset="logo-darkmode.png">
 | 
			
		||||
        <source media="(prefers-color-scheme: light)" srcset="logo.png">
 | 
			
		||||
        <img alt="CC: Tweaked" src="logo.png">
 | 
			
		||||
    </picture>
 | 
			
		||||
</h1>
 | 
			
		||||
 | 
			
		||||
CC: Tweaked is a mod for Minecraft which adds programmable computers, turtles and more to the game. A fork of the
 | 
			
		||||
much-beloved [ComputerCraft], it continues its legacy with improved performance and stability, along with a wealth of
 | 
			
		||||
new features.
 | 
			
		||||
@@ -38,7 +45,7 @@ little daunting getting started. Thankfully, there's several fantastic tutorials
 | 
			
		||||
 | 
			
		||||
 - [Direwolf20's ComputerCraft tutorials](https://www.youtube.com/watch?v=wrUHUhfCY5A "ComputerCraft Tutorial Episode 1 - HELP! and Hello World")
 | 
			
		||||
 - [Sethbling's ComputerCraft series](https://www.youtube.com/watch?v=DSsx4VSe-Uk "Programming Tutorial with Minecraft Turtles -- Ep. 1: Intro to Turtles and If-Then-Else_End")
 | 
			
		||||
 - [Lyqyd's Computer Basics 1](http://www.computercraft.info/forums2/index.php?/topic/15033-computer-basics-i/ "Computer Basics I")
 | 
			
		||||
 - [Lyqyd's Computer Basics 1](https://ccf.squiddev.cc/forums2/index.php?/topic/15033-computer-basics-i/ "Computer Basics I")
 | 
			
		||||
 | 
			
		||||
Once you're a little more familiar with the mod, the sidebar and links below provide more detailed documentation on the
 | 
			
		||||
various APIs and peripherals provided by the mod.
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ little daunting getting started. Thankfully, there's several fantastic tutorials
 | 
			
		||||
 | 
			
		||||
 - [Direwolf20's ComputerCraft tutorials](https://www.youtube.com/watch?v=wrUHUhfCY5A "ComputerCraft Tutorial Episode 1 - HELP! and Hello World")
 | 
			
		||||
 - [Sethbling's ComputerCraft series](https://www.youtube.com/watch?v=DSsx4VSe-Uk "Programming Tutorial with Minecraft Turtles -- Ep. 1: Intro to Turtles and If-Then-Else_End")
 | 
			
		||||
 - [Lyqyd's Computer Basics 1](http://www.computercraft.info/forums2/index.php?/topic/15033-computer-basics-i/ "Computer Basics I")
 | 
			
		||||
 - [Lyqyd's Computer Basics 1](https://ccf.squiddev.cc/forums2/index.php?/topic/15033-computer-basics-i/ "Computer Basics I")
 | 
			
		||||
 | 
			
		||||
Once you're a little more familiar with the mod, the [wiki](https://tweaked.cc/) provides more detailed documentation on the
 | 
			
		||||
various APIs and peripherals provided by the mod.
 | 
			
		||||
 
 | 
			
		||||
@@ -25,13 +25,13 @@ as documentation for breaking changes and "gotchas" one should look out for betw
 | 
			
		||||
 | 
			
		||||
 - Update to Lua 5.2:
 | 
			
		||||
   - Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs.
 | 
			
		||||
   - Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. `getfenv`/`setfenv`
 | 
			
		||||
     now only work on Lua functions with an `_ENV` upvalue. `getfenv` will return the global environment when called
 | 
			
		||||
     with other functions, and `setfenv` will have no effect.
 | 
			
		||||
   - `load`/`loadstring` defaults to using the global environment (`_G`) rather than the current coroutine's
 | 
			
		||||
   - Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. [`getfenv`]/[`setfenv`]
 | 
			
		||||
     now only work on Lua functions with an `_ENV` upvalue. [`getfenv`] will return the global environment when called
 | 
			
		||||
     with other functions, and [`setfenv`] will have no effect.
 | 
			
		||||
   - [`load`]/[`loadstring`] defaults to using the global environment (`_G`) rather than the current coroutine's
 | 
			
		||||
     environment.
 | 
			
		||||
   - Support for dumping functions (`string.dump`) and loading binary chunks has been removed.
 | 
			
		||||
   - `math.random` now uses Lua 5.4's random number generator.
 | 
			
		||||
   - Support for dumping functions ([`string.dump`]) and loading binary chunks has been removed.
 | 
			
		||||
   - [`math.random`] now uses Lua 5.4's random number generator.
 | 
			
		||||
 | 
			
		||||
 - File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +44,7 @@ as documentation for breaking changes and "gotchas" one should look out for betw
 | 
			
		||||
   `keys.enter` constant was queued when the key was pressed)
 | 
			
		||||
 | 
			
		||||
 - Minecraft 1.13 removed the concept of item damage and block metadata (see ["The Flattening"][flattening]). As a
 | 
			
		||||
   result `turtle.inspect` no longer provides block metadata, and `turtle.getItemDetail` no longer provides damage.
 | 
			
		||||
   result [`turtle.inspect`] no longer provides block metadata, and [`turtle.getItemDetail`] no longer provides damage.
 | 
			
		||||
 | 
			
		||||
   - Block states (`turtle.inspect().state`) should provide all the same information as block metadata, but in a much
 | 
			
		||||
     more understandable format.
 | 
			
		||||
@@ -70,12 +70,12 @@ as documentation for breaking changes and "gotchas" one should look out for betw
 | 
			
		||||
 - Unlabelled computers and turtles now keep their ID when broken, meaning that unlabelled computers/items do not stack.
 | 
			
		||||
 | 
			
		||||
## ComputerCraft 1.80pr1 {#cc-1.80}
 | 
			
		||||
 - Programs run via `shell.run` are now started in their own isolated environment. This means globals set by programs
 | 
			
		||||
 - Programs run via [`shell.run`] are now started in their own isolated environment. This means globals set by programs
 | 
			
		||||
   will not be accessible outside of this program.
 | 
			
		||||
 | 
			
		||||
 - Programs containing `/` are looked up in the current directory and are no longer looked up on the path. For instance,
 | 
			
		||||
   you can no longer type `turtle/excavate` to run `/rom/programs/turtle/excavate.lua`.
 | 
			
		||||
 | 
			
		||||
[flattening]: https://minecraft.wiki/w/Java_Edition_1.13/Flattening
 | 
			
		||||
[legal_data_pack]: https://minecraft.gamepedia.com/Tutorials/Creating_a_data_pack#Legal_characters
 | 
			
		||||
[legal_data_pack]: https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#Legal_characters
 | 
			
		||||
[datapack-example]: https://github.com/cc-tweaked/datapack-example "An example datapack for CC: Tweaked"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,11 @@ org.gradle.parallel=true
 | 
			
		||||
kotlin.stdlib.default.dependency=false
 | 
			
		||||
kotlin.jvm.target.validation.mode=error
 | 
			
		||||
 | 
			
		||||
neogradle.subsystems.conventions.runs.enabled=false
 | 
			
		||||
 | 
			
		||||
# Mod properties
 | 
			
		||||
isUnstable=true
 | 
			
		||||
modVersion=1.110.3
 | 
			
		||||
modVersion=1.111.1
 | 
			
		||||
 | 
			
		||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
 | 
			
		||||
mcVersion=1.20.5
 | 
			
		||||
mcVersion=1.21
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,14 @@
 | 
			
		||||
# 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.97.6+1.20.5"
 | 
			
		||||
fabric-loader = "0.15.10"
 | 
			
		||||
neoForge = "20.5.20-beta"
 | 
			
		||||
fabric-api = "0.100.3+1.21"
 | 
			
		||||
fabric-loader = "0.15.11"
 | 
			
		||||
neoForge = "21.0.42-beta"
 | 
			
		||||
neoForgeSpi = "8.0.1"
 | 
			
		||||
mixin = "0.8.5"
 | 
			
		||||
parchment = "2024.04.14"
 | 
			
		||||
parchmentMc = "1.20.4"
 | 
			
		||||
yarn = "1.20.5+build.1"
 | 
			
		||||
parchment = "2024.06.16"
 | 
			
		||||
parchmentMc = "1.20.6"
 | 
			
		||||
yarn = "1.21+build.1"
 | 
			
		||||
 | 
			
		||||
# Core dependencies (these versions are tied to the version Minecraft uses)
 | 
			
		||||
fastutil = "8.5.12"
 | 
			
		||||
@@ -36,14 +36,14 @@ kotlin-coroutines = "1.7.3"
 | 
			
		||||
nightConfig = "3.6.7"
 | 
			
		||||
 | 
			
		||||
# Minecraft mods
 | 
			
		||||
emi = "1.0.30+1.20.4"
 | 
			
		||||
fabricPermissions = "0.3.20230723"
 | 
			
		||||
emi = "1.1.7+1.21"
 | 
			
		||||
fabricPermissions = "0.3.1"
 | 
			
		||||
iris = "1.6.14+1.20.4"
 | 
			
		||||
jei = "17.3.0.48"
 | 
			
		||||
modmenu = "9.0.0"
 | 
			
		||||
jei = "19.0.0.1"
 | 
			
		||||
modmenu = "11.0.0-rc.4"
 | 
			
		||||
moreRed = "4.0.0.4"
 | 
			
		||||
oculus = "1.2.5"
 | 
			
		||||
rei = "14.0.688"
 | 
			
		||||
rei = "16.0.729"
 | 
			
		||||
rubidium = "0.6.1"
 | 
			
		||||
sodium = "mc1.20-0.4.10"
 | 
			
		||||
mixinExtra = "0.3.5"
 | 
			
		||||
@@ -58,20 +58,20 @@ jmh = "1.37"
 | 
			
		||||
cctJavadoc = "1.8.2"
 | 
			
		||||
checkstyle = "10.14.1"
 | 
			
		||||
curseForgeGradle = "1.1.18"
 | 
			
		||||
errorProne-core = "2.23.0"
 | 
			
		||||
errorProne-core = "2.27.0"
 | 
			
		||||
errorProne-plugin = "3.1.0"
 | 
			
		||||
fabric-loom = "1.6.7"
 | 
			
		||||
fabric-loom = "1.7.1"
 | 
			
		||||
githubRelease = "2.5.2"
 | 
			
		||||
gradleVersions = "0.50.0"
 | 
			
		||||
ideaExt = "1.1.7"
 | 
			
		||||
illuaminate = "0.1.0-71-g378d86e"
 | 
			
		||||
illuaminate = "0.1.0-73-g43ee16c"
 | 
			
		||||
lwjgl = "3.3.3"
 | 
			
		||||
minotaur = "2.8.7"
 | 
			
		||||
neoGradle = "7.0.116"
 | 
			
		||||
neoGradle = "7.0.152"
 | 
			
		||||
nullAway = "0.10.25"
 | 
			
		||||
spotless = "6.23.3"
 | 
			
		||||
taskTree = "2.1.1"
 | 
			
		||||
teavm = "0.10.0-SQUID.4"
 | 
			
		||||
teavm = "0.11.0-SQUID.1"
 | 
			
		||||
vanillaExtract = "0.1.3"
 | 
			
		||||
versionCatalogUpdate = "0.8.1"
 | 
			
		||||
 | 
			
		||||
@@ -106,9 +106,9 @@ fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fab
 | 
			
		||||
fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" }
 | 
			
		||||
emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" }
 | 
			
		||||
iris = { module = "maven.modrinth:iris", version.ref = "iris" }
 | 
			
		||||
jei-api = { module = "mezz.jei:jei-1.20.4-common-api", version.ref = "jei" }
 | 
			
		||||
jei-fabric = { module = "mezz.jei:jei-1.20.4-fabric", version.ref = "jei" }
 | 
			
		||||
jei-forge = { module = "mezz.jei:jei-1.20.4-forge", version.ref = "jei" }
 | 
			
		||||
jei-api = { module = "mezz.jei:jei-1.21-common-api", version.ref = "jei" }
 | 
			
		||||
jei-fabric = { module = "mezz.jei:jei-1.21-fabric", version.ref = "jei" }
 | 
			
		||||
jei-forge = { module = "mezz.jei:jei-1.21-neoforge", version.ref = "jei" }
 | 
			
		||||
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" }
 | 
			
		||||
@@ -179,9 +179,9 @@ kotlin = ["kotlin-stdlib", "kotlin-coroutines"]
 | 
			
		||||
# Minecraft
 | 
			
		||||
externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"]
 | 
			
		||||
externalMods-forge-compile = ["moreRed", "oculus", "jei-api"]
 | 
			
		||||
externalMods-forge-runtime = []
 | 
			
		||||
externalMods-forge-runtime = ["jei-forge"]
 | 
			
		||||
externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"]
 | 
			
		||||
externalMods-fabric-runtime = [] # ["jei-fabric", "modmenu"]
 | 
			
		||||
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
 | 
			
		||||
 | 
			
		||||
# Testing
 | 
			
		||||
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
 | 
			
		||||
networkTimeout=10000
 | 
			
		||||
validateDistributionUrl=true
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							@@ -55,7 +55,7 @@
 | 
			
		||||
#       Darwin, MinGW, and NonStop.
 | 
			
		||||
#
 | 
			
		||||
#   (3) This script is generated from the Groovy template
 | 
			
		||||
#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 | 
			
		||||
#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 | 
			
		||||
#       within the Gradle project.
 | 
			
		||||
#
 | 
			
		||||
#       You can find Gradle at https://github.com/gradle/gradle/.
 | 
			
		||||
 
 | 
			
		||||
@@ -21,4 +21,16 @@ tasks.javadoc {
 | 
			
		||||
 | 
			
		||||
    // Include the core-api in our javadoc export. This is wrong, but it means we can export a single javadoc dump.
 | 
			
		||||
    source(project(":core-api").sourceSets.main.map { it.allJava })
 | 
			
		||||
 | 
			
		||||
    options {
 | 
			
		||||
        this as StandardJavadocDocletOptions
 | 
			
		||||
        addBooleanOption("-allow-script-in-comments", true)
 | 
			
		||||
        bottom(
 | 
			
		||||
            """
 | 
			
		||||
            <script src="https://cdn.jsdelivr.net/npm/prismjs@v1.29.0/components/prism-core.min.js"></script>
 | 
			
		||||
            <script src="https://cdn.jsdelivr.net/npm/prismjs@v1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
 | 
			
		||||
            <link href=" https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css " rel="stylesheet">
 | 
			
		||||
            """.trimIndent(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,84 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
 | 
			
		||||
import net.minecraft.client.resources.model.BakedModel;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelManager;
 | 
			
		||||
import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,30 +13,47 @@ import net.minecraft.client.resources.model.ModelResourceLocation;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A model to render, combined with a transformation matrix to apply.
 | 
			
		||||
 *
 | 
			
		||||
 * @param model  The model.
 | 
			
		||||
 * @param matrix The transformation matrix.
 | 
			
		||||
 */
 | 
			
		||||
public final class TransformedModel {
 | 
			
		||||
    private final BakedModel model;
 | 
			
		||||
    private final Transformation matrix;
 | 
			
		||||
 | 
			
		||||
    public TransformedModel(BakedModel model, Transformation matrix) {
 | 
			
		||||
        this.model = Objects.requireNonNull(model);
 | 
			
		||||
        this.matrix = Objects.requireNonNull(matrix);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
public record TransformedModel(BakedModel model, Transformation matrix) {
 | 
			
		||||
    public TransformedModel(BakedModel model) {
 | 
			
		||||
        this.model = Objects.requireNonNull(model);
 | 
			
		||||
        matrix = Transformation.identity();
 | 
			
		||||
        this(model, Transformation.identity());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The location of the model to load.
 | 
			
		||||
     * @return The new {@link TransformedModel} instance.
 | 
			
		||||
     */
 | 
			
		||||
    public static TransformedModel of(ModelLocation location) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        return new TransformedModel(location.getModel(modelManager));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The location of the model to load.
 | 
			
		||||
     * @return The new {@link TransformedModel} instance.
 | 
			
		||||
     * @see ModelLocation#ofModel(ModelResourceLocation)
 | 
			
		||||
     */
 | 
			
		||||
    public static TransformedModel of(ModelResourceLocation location) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        return new TransformedModel(modelManager.getModel(location));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Look up a model in the model bakery and construct a {@link TransformedModel} with no transformation.
 | 
			
		||||
     *
 | 
			
		||||
     * @param location The location of the model to load.
 | 
			
		||||
     * @return The new {@link TransformedModel} instance.
 | 
			
		||||
     * @see ModelLocation#ofResource(ResourceLocation)
 | 
			
		||||
     */
 | 
			
		||||
    public static TransformedModel of(ResourceLocation location) {
 | 
			
		||||
        var modelManager = Minecraft.getInstance().getModelManager();
 | 
			
		||||
        return new TransformedModel(ClientPlatformHelper.get().getModel(modelManager, location));
 | 
			
		||||
@@ -46,12 +63,4 @@ public final class TransformedModel {
 | 
			
		||||
        var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(item);
 | 
			
		||||
        return new TransformedModel(model, transform);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public BakedModel getModel() {
 | 
			
		||||
        return model;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Transformation getMatrix() {
 | 
			
		||||
        return matrix;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -77,6 +78,18 @@ 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) {
 | 
			
		||||
@@ -85,7 +98,7 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
                return Stream.of(left, right);
 | 
			
		||||
                return Stream.of(left, right).flatMap(ModelLocation::getDependencies);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -17,13 +18,28 @@ import javax.annotation.Nullable;
 | 
			
		||||
@ApiStatus.Internal
 | 
			
		||||
public interface ClientPlatformHelper {
 | 
			
		||||
    /**
 | 
			
		||||
     * Equivalent to {@link ModelManager#getModel(ModelResourceLocation)} but for arbitrary {@link ResourceLocation}s.
 | 
			
		||||
     * Get a model from a resource.
 | 
			
		||||
     *
 | 
			
		||||
     * @param manager  The model manager.
 | 
			
		||||
     * @param location The model location.
 | 
			
		||||
     * @param manager          The model manager.
 | 
			
		||||
     * @param resourceLocation The model resourceLocation.
 | 
			
		||||
     * @return The baked model.
 | 
			
		||||
     * @see ModelLocation
 | 
			
		||||
     */
 | 
			
		||||
    BakedModel getModel(ModelManager manager, ResourceLocation location);
 | 
			
		||||
    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.
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ public class ComputerCraftTags {
 | 
			
		||||
        public static final TagKey<Item> DYEABLE = make("dyeable");
 | 
			
		||||
 | 
			
		||||
        private static TagKey<Item> make(String name) {
 | 
			
		||||
            return TagKey.create(Registries.ITEM, new ResourceLocation(ComputerCraftAPI.MOD_ID, name));
 | 
			
		||||
            return TagKey.create(Registries.ITEM, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -91,7 +91,7 @@ public class ComputerCraftTags {
 | 
			
		||||
        public static final TagKey<Block> TURTLE_CAN_USE = make("turtle_can_use");
 | 
			
		||||
 | 
			
		||||
        private static TagKey<Block> make(String name) {
 | 
			
		||||
            return TagKey.create(Registries.BLOCK, new ResourceLocation(ComputerCraftAPI.MOD_ID, name));
 | 
			
		||||
            return TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,13 @@ package dan200.computercraft.api.media;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.filesystem.Mount;
 | 
			
		||||
import dan200.computercraft.api.filesystem.WritableMount;
 | 
			
		||||
import net.minecraft.core.Holder;
 | 
			
		||||
import net.minecraft.core.HolderLookup;
 | 
			
		||||
import net.minecraft.server.MinecraftServer;
 | 
			
		||||
import net.minecraft.server.level.ServerLevel;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
import net.minecraft.world.item.Item;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.JukeboxSong;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
@@ -25,11 +27,12 @@ public interface IMedia {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a string representing the label of this item. Will be called via {@code disk.getLabel()} in lua.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The {@link ItemStack} to inspect.
 | 
			
		||||
     * @param registries The currently loaded registries.
 | 
			
		||||
     * @param stack      The {@link ItemStack} to inspect.
 | 
			
		||||
     * @return The label. ie: "Dan's Programs".
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    String getLabel(ItemStack stack);
 | 
			
		||||
    String getLabel(HolderLookup.Provider registries, ItemStack stack);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set a string representing the label of this item. Will be called vi {@code disk.setLabel()} in lua.
 | 
			
		||||
@@ -43,26 +46,15 @@ public interface IMedia {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * If this disk represents an item with audio (like a record), get the readable name of the audio track. ie:
 | 
			
		||||
     * "Jonathan Coulton - Still Alive"
 | 
			
		||||
     * If this disk represents an item with audio (like a record), get the corresponding {@link JukeboxSong}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The {@link ItemStack} to modify.
 | 
			
		||||
     * @return The name, or null if this item does not represent an item with audio.
 | 
			
		||||
     * @param registries The currently loaded registries.
 | 
			
		||||
     * @param stack      The {@link ItemStack} to query.
 | 
			
		||||
     * @return The song, or null if this item does not represent an item with audio.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    default String getAudioTitle(ItemStack stack) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * If this disk represents an item with audio (like a record), get the resource name of the audio track to play.
 | 
			
		||||
     *
 | 
			
		||||
     * @param stack The {@link ItemStack} to modify.
 | 
			
		||||
     * @return The name, or null if this item does not represent an item with audio.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    default SoundEvent getAudio(ItemStack stack) {
 | 
			
		||||
        return null;
 | 
			
		||||
    default Holder<JukeboxSong> getAudio(HolderLookup.Provider registries, ItemStack stack) {
 | 
			
		||||
        return JukeboxSong.fromStack(registries, stack).orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,14 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.pocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeBase;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeType;
 | 
			
		||||
import dan200.computercraft.impl.ComputerCraftAPIService;
 | 
			
		||||
import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -21,11 +23,11 @@ import javax.annotation.Nullable;
 | 
			
		||||
 * {@link UpgradeType} instance, which are then registered in a registry.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and
 | 
			
		||||
 * the upgrade automatically registered. It is recommended this is done via {@linkplain PocketUpgradeDataProvider data
 | 
			
		||||
 * generators}.
 | 
			
		||||
 * the upgrade automatically registered. It is recommended this is done via
 | 
			
		||||
 * <a href="../upgrades/UpgradeType.html#datagen">data generators</a>.
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Example</h2>
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * {@snippet lang="java" :
 | 
			
		||||
 * // We use Forge's DeferredRegister to register our upgrade type. Fabric mods may register their type directly.
 | 
			
		||||
 * static final DeferredRegister<UpgradeType<? extends IPocketUpgrade>> POCKET_UPGRADES = DeferredRegister.create(IPocketUpgrade.typeRegistry(), "my_mod");
 | 
			
		||||
 *
 | 
			
		||||
@@ -35,19 +37,19 @@ import javax.annotation.Nullable;
 | 
			
		||||
 *
 | 
			
		||||
 * // Then in your constructor
 | 
			
		||||
 * POCKET_UPGRADES.register(bus);
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * }
 | 
			
		||||
 * <p>
 | 
			
		||||
 * We can then define a new upgrade using JSON by placing the following in
 | 
			
		||||
 * {@code data/<my_mod>/computercraft/pocket_upgrades/<my_upgrade_id>.json}.
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * {@code data/<my_mod>/computercraft/pocket_upgrade/<my_upgrade_id>.json}.
 | 
			
		||||
 * {@snippet lang="json" :
 | 
			
		||||
 * {
 | 
			
		||||
 *     "type": my_mod:my_upgrade",
 | 
			
		||||
 *     "type": "my_mod:my_upgrade"
 | 
			
		||||
 * }
 | 
			
		||||
 * }
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@link PocketUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
 | 
			
		||||
 */
 | 
			
		||||
public interface IPocketUpgrade extends UpgradeBase {
 | 
			
		||||
    ResourceKey<Registry<IPocketUpgrade>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_upgrade"));
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The registry key for pocket upgrade types.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.pocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeDataProvider;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeType;
 | 
			
		||||
import dan200.computercraft.impl.ComputerCraftAPIService;
 | 
			
		||||
import dan200.computercraft.impl.RegistryHelper;
 | 
			
		||||
import net.minecraft.data.DataGenerator;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A data provider to generate pocket computer upgrades.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This should be subclassed and registered to a {@link DataGenerator.PackGenerator}. Override the
 | 
			
		||||
 * {@link #addUpgrades(Consumer)} function, construct each upgrade, and pass them off to the provided consumer to
 | 
			
		||||
 * generate them.
 | 
			
		||||
 *
 | 
			
		||||
 * @see IPocketUpgrade
 | 
			
		||||
 * @see UpgradeType
 | 
			
		||||
 */
 | 
			
		||||
public abstract class PocketUpgradeDataProvider extends UpgradeDataProvider<IPocketUpgrade> {
 | 
			
		||||
    public PocketUpgradeDataProvider(PackOutput output) {
 | 
			
		||||
        super(output, "Pocket Computer Upgrades", RegistryHelper.POCKET_UPGRADE, ComputerCraftAPIService.get().pocketUpgradeCodec());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.peripheral.IPeripheral;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeBase;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeType;
 | 
			
		||||
@@ -12,6 +13,7 @@ import net.minecraft.core.Direction;
 | 
			
		||||
import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.core.component.DataComponentPatch;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
@@ -23,11 +25,11 @@ import javax.annotation.Nullable;
 | 
			
		||||
 * {@link UpgradeType} instance, which are then registered in a registry.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and
 | 
			
		||||
 * the upgrade automatically registered. It is recommended this is done via {@linkplain TurtleUpgradeDataProvider data
 | 
			
		||||
 * generators}.
 | 
			
		||||
 * the upgrade automatically registered. It is recommended this is done via
 | 
			
		||||
 * <a href="../upgrades/UpgradeType.html#datagen">data generators</a>.
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Example</h2>
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * {@snippet lang="java" :
 | 
			
		||||
 * // We use Forge's DeferredRegister to register our upgrade type. Fabric mods may register their type directly.
 | 
			
		||||
 * static final DeferredRegister<UpgradeType<? extends ITurtleUpgrade>> TURTLE_UPGRADES = DeferredRegister.create(ITurtleUpgrade.typeRegistry(), "my_mod");
 | 
			
		||||
 *
 | 
			
		||||
@@ -37,28 +39,38 @@ import javax.annotation.Nullable;
 | 
			
		||||
 *
 | 
			
		||||
 * // Then in your constructor
 | 
			
		||||
 * TURTLE_UPGRADES.register(bus);
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * }
 | 
			
		||||
 * <p>
 | 
			
		||||
 * We can then define a new upgrade using JSON by placing the following in
 | 
			
		||||
 * {@literal data/<my_mod>/computercraft/turtle_upgrades/<my_upgrade_id>.json}}.
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * {@code data/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@snippet lang="json" :
 | 
			
		||||
 * {
 | 
			
		||||
 *     "type": "my_mod:my_upgrade"
 | 
			
		||||
 * }
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
 | 
			
		||||
 * }
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Finally, we need to register a model for our upgrade, see
 | 
			
		||||
 * {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information.
 | 
			
		||||
 *
 | 
			
		||||
 * <pre>{@code
 | 
			
		||||
 * // Register our model inside FMLClientSetupEvent
 | 
			
		||||
 * ComputerCraftAPIClient.registerTurtleUpgradeModeller(MY_UPGRADE.get(), TurtleUpgradeModeller.flatItem())
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 */
 | 
			
		||||
public interface ITurtleUpgrade extends UpgradeBase {
 | 
			
		||||
    /**
 | 
			
		||||
     * The registry in which turtle upgrades are stored.
 | 
			
		||||
     */
 | 
			
		||||
    ResourceKey<Registry<ITurtleUpgrade>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_upgrade"));
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link ResourceKey} for a turtle upgrade given a {@link ResourceLocation}.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This should only be called from within data generation code. Do not hard code references to your upgrades!
 | 
			
		||||
     *
 | 
			
		||||
     * @param id The id of the turtle upgrade.
 | 
			
		||||
     * @return The upgrade registry key.
 | 
			
		||||
     */
 | 
			
		||||
    static ResourceKey<ITurtleUpgrade> createKey(ResourceLocation id) {
 | 
			
		||||
        return ResourceKey.create(REGISTRY, id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The registry key for turtle upgrade types.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,157 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftTags;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeBase;
 | 
			
		||||
import dan200.computercraft.impl.ComputerCraftAPIService;
 | 
			
		||||
import dan200.computercraft.impl.upgrades.TurtleToolSpec;
 | 
			
		||||
import net.minecraft.core.component.DataComponents;
 | 
			
		||||
import net.minecraft.data.worldgen.BootstrapContext;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.tags.TagKey;
 | 
			
		||||
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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A builder for custom turtle tool upgrades.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This can be used from your <a href="../upgrades/UpgradeType.html#datagen">data generator</a> code in order to
 | 
			
		||||
 * register turtle tools for your mod's tools.
 | 
			
		||||
 *
 | 
			
		||||
 * <h2>Example:</h2>
 | 
			
		||||
 * {@snippet lang = "java":
 | 
			
		||||
 * import net.minecraft.data.worldgen.BootstrapContext;
 | 
			
		||||
 * import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 * import net.minecraft.world.item.Items;
 | 
			
		||||
 *
 | 
			
		||||
 * public void registerTool(BootstrapContext<ITurtleUpgrade> upgrades) {
 | 
			
		||||
 *   TurtleToolBuilder.tool(ResourceLocation.fromNamespaceAndPath("my_mod", "wooden_pickaxe"), Items.WOODEN_PICKAXE).register(upgrades);
 | 
			
		||||
 * }
 | 
			
		||||
 *}
 | 
			
		||||
 */
 | 
			
		||||
public final class TurtleToolBuilder {
 | 
			
		||||
    private final ResourceKey<ITurtleUpgrade> id;
 | 
			
		||||
    private final Item item;
 | 
			
		||||
    private Component adjective;
 | 
			
		||||
    private float damageMultiplier = TurtleToolSpec.DEFAULT_DAMAGE_MULTIPLIER;
 | 
			
		||||
    private @Nullable TagKey<Block> breakable;
 | 
			
		||||
    private boolean allowEnchantments = false;
 | 
			
		||||
    private TurtleToolDurability consumeDurability = TurtleToolDurability.NEVER;
 | 
			
		||||
 | 
			
		||||
    private TurtleToolBuilder(ResourceKey<ITurtleUpgrade> id, Item item) {
 | 
			
		||||
        this.id = id;
 | 
			
		||||
        adjective = Component.translatable(UpgradeBase.getDefaultAdjective(id.location()));
 | 
			
		||||
        this.item = item;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TurtleToolBuilder tool(ResourceLocation id, Item item) {
 | 
			
		||||
        return new TurtleToolBuilder(ITurtleUpgrade.createKey(id), item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TurtleToolBuilder tool(ResourceKey<ITurtleUpgrade> id, Item item) {
 | 
			
		||||
        return new TurtleToolBuilder(id, item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the id for this turtle tool.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The upgrade id.
 | 
			
		||||
     */
 | 
			
		||||
    public ResourceKey<ITurtleUpgrade> id() {
 | 
			
		||||
        return id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Specify a custom adjective for this tool. By default this takes its adjective from the upgrade id.
 | 
			
		||||
     *
 | 
			
		||||
     * @param adjective The new adjective to use.
 | 
			
		||||
     * @return The tool builder, for further use.
 | 
			
		||||
     */
 | 
			
		||||
    public TurtleToolBuilder adjective(Component adjective) {
 | 
			
		||||
        this.adjective = adjective;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The amount of damage a swing of this tool will do. This is multiplied by {@link Attributes#ATTACK_DAMAGE} to
 | 
			
		||||
     * get the final damage.
 | 
			
		||||
     *
 | 
			
		||||
     * @param damageMultiplier The damage multiplier.
 | 
			
		||||
     * @return The tool builder, for further use.
 | 
			
		||||
     */
 | 
			
		||||
    public TurtleToolBuilder damageMultiplier(float damageMultiplier) {
 | 
			
		||||
        this.damageMultiplier = damageMultiplier;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Indicate that this upgrade allows items which have been {@linkplain ItemStack#isEnchanted() enchanted} or have
 | 
			
		||||
     * {@linkplain DataComponents#ATTRIBUTE_MODIFIERS custom attribute modifiers}.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The tool builder, for further use.
 | 
			
		||||
     */
 | 
			
		||||
    public TurtleToolBuilder allowEnchantments() {
 | 
			
		||||
        allowEnchantments = true;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set when the tool will consume durability.
 | 
			
		||||
     *
 | 
			
		||||
     * @param durability The durability predicate.
 | 
			
		||||
     * @return The tool builder, for further use.
 | 
			
		||||
     */
 | 
			
		||||
    public TurtleToolBuilder consumeDurability(TurtleToolDurability durability) {
 | 
			
		||||
        consumeDurability = durability;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Provide a list of breakable blocks. If not given, the tool can break all blocks. If given, only blocks
 | 
			
		||||
     * in this tag, those in {@link ComputerCraftTags.Blocks#TURTLE_ALWAYS_BREAKABLE} and "insta-mine" ones can
 | 
			
		||||
     * be broken.
 | 
			
		||||
     *
 | 
			
		||||
     * @param breakable The tag containing all blocks breakable by this item.
 | 
			
		||||
     * @return The tool builder, for further use.
 | 
			
		||||
     * @see ComputerCraftTags.Blocks
 | 
			
		||||
     */
 | 
			
		||||
    public TurtleToolBuilder breakable(TagKey<Block> breakable) {
 | 
			
		||||
        this.breakable = breakable;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Build the turtle tool upgrade.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The constructed upgrade.
 | 
			
		||||
     */
 | 
			
		||||
    public ITurtleUpgrade build() {
 | 
			
		||||
        return ComputerCraftAPIService.get().createTurtleTool(new TurtleToolSpec(
 | 
			
		||||
            adjective,
 | 
			
		||||
            item,
 | 
			
		||||
            damageMultiplier,
 | 
			
		||||
            allowEnchantments,
 | 
			
		||||
            consumeDurability,
 | 
			
		||||
            Optional.ofNullable(breakable)
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Build this upgrade and register it for datagen.
 | 
			
		||||
     *
 | 
			
		||||
     * @param upgrades The registry this upgrade should be added to.
 | 
			
		||||
     */
 | 
			
		||||
    public void register(BootstrapContext<ITurtleUpgrade> upgrades) {
 | 
			
		||||
        upgrades.register(id(), build());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,7 +11,7 @@ import net.minecraft.world.item.ItemStack;
 | 
			
		||||
/**
 | 
			
		||||
 * Indicates if an equipped turtle item will consume durability.
 | 
			
		||||
 *
 | 
			
		||||
 * @see TurtleUpgradeDataProvider.ToolBuilder#consumeDurability(TurtleToolDurability)
 | 
			
		||||
 * @see TurtleToolBuilder#consumeDurability(TurtleToolDurability)
 | 
			
		||||
 */
 | 
			
		||||
public enum TurtleToolDurability implements StringRepresentable {
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,164 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftTags;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeBase;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeDataProvider;
 | 
			
		||||
import dan200.computercraft.impl.ComputerCraftAPIService;
 | 
			
		||||
import dan200.computercraft.impl.RegistryHelper;
 | 
			
		||||
import dan200.computercraft.impl.upgrades.TurtleToolSpec;
 | 
			
		||||
import net.minecraft.core.component.DataComponents;
 | 
			
		||||
import net.minecraft.data.DataGenerator;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.tags.TagKey;
 | 
			
		||||
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 javax.annotation.Nullable;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A data provider to generate turtle upgrades.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This should be subclassed and registered to a {@link DataGenerator.PackGenerator}. Override the
 | 
			
		||||
 * {@link #addUpgrades(Consumer)} function, construct each upgrade, and pass them off to the provided consumer to
 | 
			
		||||
 * generate them.
 | 
			
		||||
 *
 | 
			
		||||
 * @see ITurtleUpgrade
 | 
			
		||||
 */
 | 
			
		||||
public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITurtleUpgrade> {
 | 
			
		||||
    public TurtleUpgradeDataProvider(PackOutput output) {
 | 
			
		||||
        super(output, "Turtle Upgrades", RegistryHelper.TURTLE_UPGRADE, ComputerCraftAPIService.get().turtleUpgradeCodec());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new turtle tool upgrade, such as a pickaxe or shovel.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id   The ID of this tool.
 | 
			
		||||
     * @param item The item used for tool actions. Note, this doesn't inherit all properties of the tool, you may need
 | 
			
		||||
     *             to specify {@link ToolBuilder#damageMultiplier(float)} and {@link ToolBuilder#breakable(TagKey)}.
 | 
			
		||||
     * @return A tool builder,
 | 
			
		||||
     */
 | 
			
		||||
    public final ToolBuilder tool(ResourceLocation id, Item item) {
 | 
			
		||||
        return new ToolBuilder(id, item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A builder for custom turtle tool upgrades.
 | 
			
		||||
     *
 | 
			
		||||
     * @see #tool(ResourceLocation, Item)
 | 
			
		||||
     */
 | 
			
		||||
    public final class ToolBuilder {
 | 
			
		||||
        private final ResourceLocation id;
 | 
			
		||||
        private final Item toolItem;
 | 
			
		||||
        private Component adjective;
 | 
			
		||||
        private @Nullable Item craftingItem;
 | 
			
		||||
        private float damageMultiplier = TurtleToolSpec.DEFAULT_DAMAGE_MULTIPLIER;
 | 
			
		||||
        private @Nullable TagKey<Block> breakable;
 | 
			
		||||
        private boolean allowEnchantments = false;
 | 
			
		||||
        private TurtleToolDurability consumeDurability = TurtleToolDurability.NEVER;
 | 
			
		||||
 | 
			
		||||
        ToolBuilder(ResourceLocation id, Item toolItem) {
 | 
			
		||||
            this.id = id;
 | 
			
		||||
            adjective = Component.translatable(UpgradeBase.getDefaultAdjective(id));
 | 
			
		||||
            this.toolItem = toolItem;
 | 
			
		||||
            craftingItem = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Specify a custom adjective for this tool. By default this takes its adjective from the tool item.
 | 
			
		||||
         *
 | 
			
		||||
         * @param adjective The new adjective to use.
 | 
			
		||||
         * @return The tool builder, for further use.
 | 
			
		||||
         */
 | 
			
		||||
        public ToolBuilder adjective(Component adjective) {
 | 
			
		||||
            this.adjective = adjective;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Specify a custom item which is used to craft this upgrade. By default this is the same as the provided tool
 | 
			
		||||
         * item, but you may wish to override it.
 | 
			
		||||
         *
 | 
			
		||||
         * @param craftingItem The item used to craft this upgrade.
 | 
			
		||||
         * @return The tool builder, for further use.
 | 
			
		||||
         */
 | 
			
		||||
        public ToolBuilder craftingItem(Item craftingItem) {
 | 
			
		||||
            this.craftingItem = craftingItem;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The amount of damage a swing of this tool will do. This is multiplied by {@link Attributes#ATTACK_DAMAGE} to
 | 
			
		||||
         * get the final damage.
 | 
			
		||||
         *
 | 
			
		||||
         * @param damageMultiplier The damage multiplier.
 | 
			
		||||
         * @return The tool builder, for further use.
 | 
			
		||||
         */
 | 
			
		||||
        public ToolBuilder damageMultiplier(float damageMultiplier) {
 | 
			
		||||
            this.damageMultiplier = damageMultiplier;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Indicate that this upgrade allows items which have been {@linkplain ItemStack#isEnchanted() enchanted} or have
 | 
			
		||||
         * {@linkplain DataComponents#ATTRIBUTE_MODIFIERS custom attribute modifiers}.
 | 
			
		||||
         *
 | 
			
		||||
         * @return The tool builder, for further use.
 | 
			
		||||
         */
 | 
			
		||||
        public ToolBuilder allowEnchantments() {
 | 
			
		||||
            allowEnchantments = true;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Set when the tool will consume durability.
 | 
			
		||||
         *
 | 
			
		||||
         * @param durability The durability predicate.
 | 
			
		||||
         * @return The tool builder, for further use.
 | 
			
		||||
         */
 | 
			
		||||
        public ToolBuilder consumeDurability(TurtleToolDurability durability) {
 | 
			
		||||
            consumeDurability = durability;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Provide a list of breakable blocks. If not given, the tool can break all blocks. If given, only blocks
 | 
			
		||||
         * in this tag, those in {@link ComputerCraftTags.Blocks#TURTLE_ALWAYS_BREAKABLE} and "insta-mine" ones can
 | 
			
		||||
         * be broken.
 | 
			
		||||
         *
 | 
			
		||||
         * @param breakable The tag containing all blocks breakable by this item.
 | 
			
		||||
         * @return The tool builder, for further use.
 | 
			
		||||
         * @see ComputerCraftTags.Blocks
 | 
			
		||||
         */
 | 
			
		||||
        public ToolBuilder breakable(TagKey<Block> breakable) {
 | 
			
		||||
            this.breakable = breakable;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Register this as an upgrade.
 | 
			
		||||
         *
 | 
			
		||||
         * @param add The callback given to {@link #addUpgrades(Consumer)}.
 | 
			
		||||
         */
 | 
			
		||||
        public void add(Consumer<Upgrade<ITurtleUpgrade>> add) {
 | 
			
		||||
            upgrade(id, ComputerCraftAPIService.get().createTurtleTool(new TurtleToolSpec(
 | 
			
		||||
                adjective,
 | 
			
		||||
                Optional.ofNullable(craftingItem),
 | 
			
		||||
                toolItem,
 | 
			
		||||
                damageMultiplier,
 | 
			
		||||
                allowEnchantments,
 | 
			
		||||
                consumeDurability,
 | 
			
		||||
                Optional.ofNullable(breakable)
 | 
			
		||||
            ))).add(add);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,154 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.upgrades;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import com.mojang.serialization.Codec;
 | 
			
		||||
import com.mojang.serialization.JsonOps;
 | 
			
		||||
import dan200.computercraft.impl.PlatformHelper;
 | 
			
		||||
import net.minecraft.Util;
 | 
			
		||||
import net.minecraft.core.Registry;
 | 
			
		||||
import net.minecraft.data.CachedOutput;
 | 
			
		||||
import net.minecraft.data.DataProvider;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.resources.ResourceKey;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A data generator/provider for turtle and pocket computer upgrades. This should not be extended directly, instead see
 | 
			
		||||
 * the other subclasses.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The base class of upgrades.
 | 
			
		||||
 */
 | 
			
		||||
public abstract class UpgradeDataProvider<T extends UpgradeBase> implements DataProvider {
 | 
			
		||||
    private final PackOutput output;
 | 
			
		||||
    private final String name;
 | 
			
		||||
    private final ResourceKey<Registry<T>> registryName;
 | 
			
		||||
    private final Codec<T> codec;
 | 
			
		||||
 | 
			
		||||
    private @Nullable Map<ResourceKey<T>, T> upgrades;
 | 
			
		||||
 | 
			
		||||
    @ApiStatus.Internal
 | 
			
		||||
    protected UpgradeDataProvider(PackOutput output, String name, ResourceKey<Registry<T>> registryName, Codec<T> codec) {
 | 
			
		||||
        this.output = output;
 | 
			
		||||
        this.name = name;
 | 
			
		||||
        this.registryName = registryName;
 | 
			
		||||
        this.codec = codec;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a new upgrade.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id      The ID of the upgrade to create.
 | 
			
		||||
     * @param upgrade The upgrade to add.
 | 
			
		||||
     * @return The constructed upgrade, ready to be passed off to {@link #addUpgrades(Consumer)}'s consumer.
 | 
			
		||||
     */
 | 
			
		||||
    protected final Upgrade<T> upgrade(ResourceLocation id, T upgrade) {
 | 
			
		||||
        return new Upgrade<>(id, upgrade, j -> {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add all turtle or pocket computer upgrades.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * <strong>Example usage:</strong>
 | 
			
		||||
     * <pre>{@code
 | 
			
		||||
     * protected void addUpgrades(Consumer<Upgrade<ITurtleUpgrade>> addUpgrade) {
 | 
			
		||||
     *     upgrade(new ResourceLocation("mymod", "speaker"), new TurtleSpeaker(new ItemStack(Items.NOTE_BLOCK))).add(addUpgrade);
 | 
			
		||||
     * }
 | 
			
		||||
     * }</pre>
 | 
			
		||||
     *
 | 
			
		||||
     * @param addUpgrade A callback used to register an upgrade.
 | 
			
		||||
     */
 | 
			
		||||
    protected abstract void addUpgrades(Consumer<Upgrade<T>> addUpgrade);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public CompletableFuture<?> run(CachedOutput cache) {
 | 
			
		||||
        var base = output.createPathProvider(PackOutput.Target.DATA_PACK, registryName.location().getNamespace() + "/" + registryName.location().getPath());
 | 
			
		||||
 | 
			
		||||
        Map<ResourceKey<T>, T> upgrades = new LinkedHashMap<>();
 | 
			
		||||
 | 
			
		||||
        List<CompletableFuture<?>> futures = new ArrayList<>();
 | 
			
		||||
        addUpgrades(upgrade -> {
 | 
			
		||||
            var id = ResourceKey.create(registryName, upgrade.id);
 | 
			
		||||
            if (upgrades.containsKey(id)) throw new IllegalStateException("Duplicate upgrade " + upgrade.id);
 | 
			
		||||
 | 
			
		||||
            var json = (JsonObject) codec.encodeStart(JsonOps.INSTANCE, upgrade.upgrade).getOrThrow();
 | 
			
		||||
            upgrade.serialise.accept(json);
 | 
			
		||||
 | 
			
		||||
            futures.add(DataProvider.saveStable(cache, json, base.json(upgrade.id)));
 | 
			
		||||
 | 
			
		||||
            upgrades.put(id, upgrade.upgrade);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.upgrades = Collections.unmodifiableMap(upgrades);
 | 
			
		||||
 | 
			
		||||
        return Util.sequenceFailFast(futures);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final String getName() {
 | 
			
		||||
        return name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all registered upgrades.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The map of registered upgrades.
 | 
			
		||||
     */
 | 
			
		||||
    public Map<ResourceKey<T>, T> getGeneratedUpgrades() {
 | 
			
		||||
        var upgrades = this.upgrades;
 | 
			
		||||
        if (upgrades == null) throw new IllegalStateException("Upgrades are not available yet");
 | 
			
		||||
        return upgrades;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A constructed upgrade instance, produced {@link #addUpgrades(Consumer)}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param <T> The type of upgrade.
 | 
			
		||||
     */
 | 
			
		||||
    public static final class Upgrade<T extends UpgradeBase> {
 | 
			
		||||
        private final ResourceLocation id;
 | 
			
		||||
        private final T upgrade;
 | 
			
		||||
        private final Consumer<JsonObject> serialise;
 | 
			
		||||
 | 
			
		||||
        private Upgrade(ResourceLocation id, T upgrade, Consumer<JsonObject> serialise) {
 | 
			
		||||
            this.id = id;
 | 
			
		||||
            this.upgrade = upgrade;
 | 
			
		||||
            this.serialise = serialise;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Convenience method for registering an upgrade.
 | 
			
		||||
         *
 | 
			
		||||
         * @param add The callback given to {@link #addUpgrades(Consumer)}
 | 
			
		||||
         */
 | 
			
		||||
        public void add(Consumer<Upgrade<T>> add) {
 | 
			
		||||
            add.accept(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Return a new {@link Upgrade} which requires the given mod to be present.
 | 
			
		||||
         * <p>
 | 
			
		||||
         * This uses mod-loader-specific hooks (Forge's crafting conditions and Fabric's resource conditions). If using
 | 
			
		||||
         * this in a multi-loader setup, you must generate resources separately for the two loaders.
 | 
			
		||||
         *
 | 
			
		||||
         * @param modId The id of the mod.
 | 
			
		||||
         * @return A new upgrade instance.
 | 
			
		||||
         */
 | 
			
		||||
        public Upgrade<T> requireMod(String modId) {
 | 
			
		||||
            return new Upgrade<>(id, upgrade, json -> {
 | 
			
		||||
                PlatformHelper.get().addRequiredModCondition(json, modId);
 | 
			
		||||
                serialise.accept(json);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,11 +6,10 @@ package dan200.computercraft.api.upgrades;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.MapCodec;
 | 
			
		||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
 | 
			
		||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
 | 
			
		||||
import dan200.computercraft.impl.upgrades.UpgradeTypeImpl;
 | 
			
		||||
import net.minecraft.core.registries.BuiltInRegistries;
 | 
			
		||||
import net.minecraft.data.registries.RegistryPatchGenerator;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.Recipe;
 | 
			
		||||
import net.minecraft.world.level.storage.loot.functions.LootItemFunction;
 | 
			
		||||
@@ -33,8 +32,35 @@ import java.util.function.Function;
 | 
			
		||||
 * {@link IPocketUpgrade#typeRegistry()}).
 | 
			
		||||
 * <p>
 | 
			
		||||
 * In order to register the actual upgrade, a JSON file referencing your upgrade type should be added to a datapack. It
 | 
			
		||||
 * is recommended to do this via the data generators (see {@link TurtleUpgradeDataProvider} and
 | 
			
		||||
 * {@link PocketUpgradeDataProvider}).
 | 
			
		||||
 * is recommended to do this via the data generators.
 | 
			
		||||
 *
 | 
			
		||||
 * <h2 id="datagen">Data Generation</h2>
 | 
			
		||||
 * As turtle and pocket upgrades are just loaded using vanilla's dynamic loaders, one may use the same data generation
 | 
			
		||||
 * tools as you would for any other dynamic registry.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This can typically be done by extending vanilla's built-in registries using {@link RegistryPatchGenerator}, and then
 | 
			
		||||
 * writing out the new registries using {@code net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider}
 | 
			
		||||
 * on Fabric or {@code net.neoforged.neoforge.common.data.DatapackBuiltinEntriesProvider} on Forge.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@snippet lang="java" :
 | 
			
		||||
 * import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
 * import net.minecraft.Util;
 | 
			
		||||
 * import net.minecraft.core.HolderLookup;
 | 
			
		||||
 * import net.minecraft.core.RegistrySetBuilder;
 | 
			
		||||
 * import net.minecraft.data.DataGenerator;
 | 
			
		||||
 * import net.neoforged.neoforge.common.data.DatapackBuiltinEntriesProvider;
 | 
			
		||||
 *
 | 
			
		||||
 * import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 *
 | 
			
		||||
 * public void generate(DataGenerator.PackGenerator output, CompletableFuture<HolderLookup.Provider> registries) {
 | 
			
		||||
 *     var newRegistries = RegistryPatchGenerator.createLookup(registries, Util.make(new RegistrySetBuilder(), builder -> {
 | 
			
		||||
 *         builder.add(ITurtleUpgrade.REGISTRY, upgrades -> {
 | 
			
		||||
 *             upgrades.register(ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath("my_mod", "my_upgrade")), new MyUpgrade());
 | 
			
		||||
 *         });
 | 
			
		||||
 *     }));
 | 
			
		||||
 *     output.addProvider(o -> new DatapackBuiltinEntriesProvider(o, newRegistries, Set.of("my_mod")));
 | 
			
		||||
 * }
 | 
			
		||||
 * }
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The upgrade subclass that this upgrade type represents.
 | 
			
		||||
 * @see ITurtleUpgrade
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.impl;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonObject;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeDataProvider;
 | 
			
		||||
import org.jetbrains.annotations.ApiStatus;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstraction layer for Forge and Fabric. See implementations for more details.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Do <strong>NOT</strong> directly reference this class. It exists for internal use by the API.
 | 
			
		||||
 */
 | 
			
		||||
@ApiStatus.Internal
 | 
			
		||||
public interface PlatformHelper {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the current {@link PlatformHelper} instance.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The current instance.
 | 
			
		||||
     */
 | 
			
		||||
    static PlatformHelper get() {
 | 
			
		||||
        var instance = Instance.INSTANCE;
 | 
			
		||||
        return instance == null ? Services.raise(PlatformHelper.class, Instance.ERROR) : instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a resource condition which requires a mod to be loaded. This should be used by data providers such as
 | 
			
		||||
     * {@link UpgradeDataProvider}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param object The JSON object we're generating.
 | 
			
		||||
     * @param modId  The mod ID that we require.
 | 
			
		||||
     */
 | 
			
		||||
    void addRequiredModCondition(JsonObject object, String modId);
 | 
			
		||||
 | 
			
		||||
    final class Instance {
 | 
			
		||||
        static final @Nullable PlatformHelper INSTANCE;
 | 
			
		||||
        static final @Nullable Throwable ERROR;
 | 
			
		||||
 | 
			
		||||
        static {
 | 
			
		||||
            // We don't want class initialisation to fail here (as that results in confusing errors). Instead, capture
 | 
			
		||||
            // the error and rethrow it when accessing. This should be JITted away in the common case.
 | 
			
		||||
            var helper = Services.tryLoad(PlatformHelper.class);
 | 
			
		||||
            INSTANCE = helper.instance();
 | 
			
		||||
            ERROR = helper.error();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Instance() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,8 +22,7 @@ import java.util.Optional;
 | 
			
		||||
 * The template for a turtle tool.
 | 
			
		||||
 *
 | 
			
		||||
 * @param adjective         The adjective for this tool.
 | 
			
		||||
 * @param craftItem         The item used to craft this tool.
 | 
			
		||||
 * @param toolItem          The actual tool used.
 | 
			
		||||
 * @param item              The tool used.
 | 
			
		||||
 * @param damageMultiplier  The damage multiplier for this tool.
 | 
			
		||||
 * @param allowEnchantments Whether to allow enchantments.
 | 
			
		||||
 * @param consumeDurability When to consume durability.
 | 
			
		||||
@@ -31,8 +30,7 @@ import java.util.Optional;
 | 
			
		||||
 */
 | 
			
		||||
public record TurtleToolSpec(
 | 
			
		||||
    Component adjective,
 | 
			
		||||
    Optional<Item> craftItem,
 | 
			
		||||
    Item toolItem,
 | 
			
		||||
    Item item,
 | 
			
		||||
    float damageMultiplier,
 | 
			
		||||
    boolean allowEnchantments,
 | 
			
		||||
    TurtleToolDurability consumeDurability,
 | 
			
		||||
@@ -42,8 +40,7 @@ public record TurtleToolSpec(
 | 
			
		||||
 | 
			
		||||
    public static final MapCodec<TurtleToolSpec> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
 | 
			
		||||
        ComponentSerialization.CODEC.fieldOf("adjective").forGetter(TurtleToolSpec::adjective),
 | 
			
		||||
        BuiltInRegistries.ITEM.byNameCodec().optionalFieldOf("craftingItem").forGetter(TurtleToolSpec::craftItem),
 | 
			
		||||
        BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(TurtleToolSpec::toolItem),
 | 
			
		||||
        BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(TurtleToolSpec::item),
 | 
			
		||||
        Codec.FLOAT.optionalFieldOf("damageMultiplier", DEFAULT_DAMAGE_MULTIPLIER).forGetter(TurtleToolSpec::damageMultiplier),
 | 
			
		||||
        Codec.BOOL.optionalFieldOf("allowEnchantments", false).forGetter(TurtleToolSpec::allowEnchantments),
 | 
			
		||||
        TurtleToolDurability.CODEC.optionalFieldOf("consumeDurability", TurtleToolDurability.NEVER).forGetter(TurtleToolSpec::consumeDurability),
 | 
			
		||||
 
 | 
			
		||||
@@ -129,3 +129,5 @@ val runData by tasks.registering(MergeTrees::class) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,8 @@ 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.computer.inventory.ViewComputerMenu;
 | 
			
		||||
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;
 | 
			
		||||
@@ -54,6 +54,7 @@ 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;
 | 
			
		||||
@@ -101,15 +102,12 @@ public final class ClientRegistry {
 | 
			
		||||
 | 
			
		||||
    public static void registerMenuScreens(RegisterMenuScreen register) {
 | 
			
		||||
        register.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
 | 
			
		||||
        register.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER.get(), ComputerScreen::new);
 | 
			
		||||
        register.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
 | 
			
		||||
        register.register(ModRegistry.Menus.TURTLE.get(), TurtleScreen::new);
 | 
			
		||||
 | 
			
		||||
        register.register(ModRegistry.Menus.PRINTER.get(), PrinterScreen::new);
 | 
			
		||||
        register.register(ModRegistry.Menus.DISK_DRIVE.get(), DiskDriveScreen::new);
 | 
			
		||||
        register.register(ModRegistry.Menus.PRINTOUT.get(), PrintoutScreen::new);
 | 
			
		||||
 | 
			
		||||
        register.<ViewComputerMenu, ComputerScreen<ViewComputerMenu>>register(ModRegistry.Menus.VIEW_COMPUTER.get(), ComputerScreen::new);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface RegisterMenuScreen {
 | 
			
		||||
@@ -118,12 +116,12 @@ public final class ClientRegistry {
 | 
			
		||||
 | 
			
		||||
    public static void registerTurtleModellers(RegisterTurtleUpgradeModeller register) {
 | 
			
		||||
        register.register(ModRegistry.TurtleUpgradeTypes.SPEAKER.get(), TurtleUpgradeModeller.sided(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
 | 
			
		||||
        ));
 | 
			
		||||
        register.register(ModRegistry.TurtleUpgradeTypes.WORKBENCH.get(), TurtleUpgradeModeller.sided(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
 | 
			
		||||
        ));
 | 
			
		||||
        register.register(ModRegistry.TurtleUpgradeTypes.WIRELESS_MODEM.get(), new TurtleModemModeller());
 | 
			
		||||
        register.register(ModRegistry.TurtleUpgradeTypes.TOOL.get(), TurtleUpgradeModeller.flatItem());
 | 
			
		||||
@@ -131,7 +129,7 @@ public final class ClientRegistry {
 | 
			
		||||
 | 
			
		||||
    @SafeVarargs
 | 
			
		||||
    private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
 | 
			
		||||
        var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
 | 
			
		||||
        var id = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
 | 
			
		||||
        for (var item : items) itemProperties.register(item.get(), id, getter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -147,15 +145,14 @@ public final class ClientRegistry {
 | 
			
		||||
        register.accept(GuiSprites.initialise(minecraft.getTextureManager()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final String[] EXTRA_MODELS = new String[]{
 | 
			
		||||
        "block/turtle_colour",
 | 
			
		||||
        "block/turtle_elf_overlay",
 | 
			
		||||
        "block/turtle_rainbow_overlay",
 | 
			
		||||
        "block/turtle_trans_overlay",
 | 
			
		||||
    private static final ResourceLocation[] EXTRA_MODELS = {
 | 
			
		||||
        TurtleOverlay.ELF_MODEL,
 | 
			
		||||
        TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public static void registerExtraModels(Consumer<ResourceLocation> register) {
 | 
			
		||||
        for (var model : EXTRA_MODELS) register.accept(new ResourceLocation(ComputerCraftAPI.MOD_ID, model));
 | 
			
		||||
    public static void registerExtraModels(Consumer<ResourceLocation> register, Collection<ResourceLocation> extraModels) {
 | 
			
		||||
        for (var model : EXTRA_MODELS) register.accept(model);
 | 
			
		||||
        extraModels.forEach(register);
 | 
			
		||||
        TurtleUpgradeModellers.getDependencies().forEach(register);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
 * The GUI for disk drives.
 | 
			
		||||
 */
 | 
			
		||||
public class DiskDriveScreen extends AbstractContainerScreen<DiskDriveMenu> {
 | 
			
		||||
    private static final ResourceLocation BACKGROUND = new ResourceLocation("computercraft", "textures/gui/disk_drive.png");
 | 
			
		||||
    private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/disk_drive.png");
 | 
			
		||||
 | 
			
		||||
    public DiskDriveScreen(DiskDriveMenu container, Inventory player, Component title) {
 | 
			
		||||
        super(container, player, title);
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import java.util.stream.Stream;
 | 
			
		||||
 * Sprite sheet for all GUI texutres in the mod.
 | 
			
		||||
 */
 | 
			
		||||
public final class GuiSprites extends TextureAtlasHolder {
 | 
			
		||||
    public static final ResourceLocation SPRITE_SHEET = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui");
 | 
			
		||||
    public static final ResourceLocation SPRITE_SHEET = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui");
 | 
			
		||||
    public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png");
 | 
			
		||||
 | 
			
		||||
    public static final ButtonTextures TURNED_OFF = button("turned_off");
 | 
			
		||||
@@ -35,16 +35,16 @@ public final class GuiSprites extends TextureAtlasHolder {
 | 
			
		||||
 | 
			
		||||
    private static ButtonTextures button(String name) {
 | 
			
		||||
        return new ButtonTextures(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ComputerTextures computer(String name, boolean pocket, boolean sidebar) {
 | 
			
		||||
        return new ComputerTextures(
 | 
			
		||||
            new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/border_" + name),
 | 
			
		||||
            pocket ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/pocket_bottom_" + name) : null,
 | 
			
		||||
            sidebar ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sidebar_" + name) : null
 | 
			
		||||
            ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/border_" + name),
 | 
			
		||||
            pocket ? ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/pocket_bottom_" + name) : null,
 | 
			
		||||
            sidebar ? ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "gui/sidebar_" + name) : null
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import java.util.List;
 | 
			
		||||
 * A {@link Toast} implementation which displays an arbitrary message along with an optional {@link ItemStack}.
 | 
			
		||||
 */
 | 
			
		||||
public class ItemToast implements Toast {
 | 
			
		||||
    private static final ResourceLocation TEXTURE = new ResourceLocation("toast/recipe");
 | 
			
		||||
    private static final ResourceLocation TEXTURE = ResourceLocation.withDefaultNamespace("toast/recipe");
 | 
			
		||||
    public static final Object TRANSFER_NO_RESPONSE_TOKEN = new Object();
 | 
			
		||||
 | 
			
		||||
    private static final long DISPLAY_TIME = 7000L;
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import static dan200.computercraft.core.util.Nullability.assertNonNull;
 | 
			
		||||
 * When closed, it returns to the previous screen.
 | 
			
		||||
 */
 | 
			
		||||
public final class OptionScreen extends Screen {
 | 
			
		||||
    private static final ResourceLocation BACKGROUND = new ResourceLocation("computercraft", "textures/gui/blank_screen.png");
 | 
			
		||||
    private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/blank_screen.png");
 | 
			
		||||
 | 
			
		||||
    public static final int BUTTON_WIDTH = 100;
 | 
			
		||||
    public static final int BUTTON_HEIGHT = 20;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
 * The GUI for printers.
 | 
			
		||||
 */
 | 
			
		||||
public class PrinterScreen extends AbstractContainerScreen<PrinterMenu> {
 | 
			
		||||
    private static final ResourceLocation BACKGROUND = new ResourceLocation("computercraft", "textures/gui/printer.png");
 | 
			
		||||
    private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printer.png");
 | 
			
		||||
 | 
			
		||||
    public PrinterScreen(PrinterMenu container, Inventory player, Component title) {
 | 
			
		||||
        super(container, player, title);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.Tesselator;
 | 
			
		||||
import dan200.computercraft.core.terminal.TextBuffer;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.common.HeldItemMenu;
 | 
			
		||||
@@ -12,7 +11,6 @@ import dan200.computercraft.shared.media.items.PrintoutData;
 | 
			
		||||
import dan200.computercraft.shared.media.items.PrintoutItem;
 | 
			
		||||
import net.minecraft.client.gui.GuiGraphics;
 | 
			
		||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import net.minecraft.world.entity.player.Inventory;
 | 
			
		||||
import org.lwjgl.glfw.GLFW;
 | 
			
		||||
@@ -90,10 +88,8 @@ public class PrintoutScreen extends AbstractContainerScreen<HeldItemMenu> {
 | 
			
		||||
        graphics.pose().pushPose();
 | 
			
		||||
        graphics.pose().translate(0, 0, 1);
 | 
			
		||||
 | 
			
		||||
        var renderer = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
 | 
			
		||||
        drawBorder(graphics.pose(), renderer, leftPos, topPos, 0, page, pages, book, FULL_BRIGHT_LIGHTMAP);
 | 
			
		||||
        drawText(graphics.pose(), renderer, leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, text, colours);
 | 
			
		||||
        renderer.endBatch();
 | 
			
		||||
        drawBorder(graphics.pose(), graphics.bufferSource(), leftPos, topPos, 0, page, pages, 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, text, colours);
 | 
			
		||||
 | 
			
		||||
        graphics.pose().popPose();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ import static dan200.computercraft.shared.turtle.inventory.TurtleMenu.*;
 | 
			
		||||
 * The GUI for turtles.
 | 
			
		||||
 */
 | 
			
		||||
public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
 | 
			
		||||
    private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
 | 
			
		||||
    private static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
 | 
			
		||||
    private static final ResourceLocation BACKGROUND_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
 | 
			
		||||
    private static final ResourceLocation BACKGROUND_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
 | 
			
		||||
 | 
			
		||||
    private static final int TEX_WIDTH = 278;
 | 
			
		||||
    private static final int TEX_HEIGHT = 217;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.gui.widgets;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.Tesselator;
 | 
			
		||||
import dan200.computercraft.client.render.RenderTypes;
 | 
			
		||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
 | 
			
		||||
import dan200.computercraft.core.terminal.Terminal;
 | 
			
		||||
@@ -16,7 +15,6 @@ import net.minecraft.client.gui.components.AbstractWidget;
 | 
			
		||||
import net.minecraft.client.gui.narration.NarratedElementType;
 | 
			
		||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
 | 
			
		||||
import net.minecraft.client.gui.screens.Screen;
 | 
			
		||||
import net.minecraft.client.renderer.MultiBufferSource;
 | 
			
		||||
import net.minecraft.network.chat.Component;
 | 
			
		||||
import org.lwjgl.glfw.GLFW;
 | 
			
		||||
 | 
			
		||||
@@ -259,15 +257,12 @@ public class TerminalWidget extends AbstractWidget {
 | 
			
		||||
    public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
 | 
			
		||||
        if (!visible) return;
 | 
			
		||||
 | 
			
		||||
        var bufferSource = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
 | 
			
		||||
        var emitter = FixedWidthFontRenderer.toVertexConsumer(graphics.pose(), bufferSource.getBuffer(RenderTypes.TERMINAL));
 | 
			
		||||
        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
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        bufferSource.endBatch();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,15 @@ package dan200.computercraft.client.integration.emi;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.integration.RecipeModHelpers;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
import dev.emi.emi.api.EmiEntrypoint;
 | 
			
		||||
import dev.emi.emi.api.EmiPlugin;
 | 
			
		||||
import dev.emi.emi.api.EmiRegistry;
 | 
			
		||||
import dev.emi.emi.api.stack.Comparison;
 | 
			
		||||
import dev.emi.emi.api.stack.EmiStack;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
import java.util.function.BiPredicate;
 | 
			
		||||
@@ -25,6 +28,10 @@ public class EMIComputerCraft implements EmiPlugin {
 | 
			
		||||
 | 
			
		||||
        registry.setDefaultComparison(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), pocketComparison);
 | 
			
		||||
        registry.setDefaultComparison(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(), pocketComparison);
 | 
			
		||||
 | 
			
		||||
        for (var stack : RecipeModHelpers.getExtraStacks(Minecraft.getInstance().level.registryAccess())) {
 | 
			
		||||
            registry.addEmiStack(EmiStack.of(stack));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final Comparison turtleComparison = compareStacks((left, right)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.integration.jei;
 | 
			
		||||
package dan200.computercraft.client.integration.jei;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
@@ -19,6 +19,8 @@ import mezz.jei.api.ingredients.subtypes.IIngredientSubtypeInterpreter;
 | 
			
		||||
import mezz.jei.api.registration.IAdvancedRegistration;
 | 
			
		||||
import mezz.jei.api.registration.ISubtypeRegistration;
 | 
			
		||||
import mezz.jei.api.runtime.IJeiRuntime;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.core.RegistryAccess;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
 | 
			
		||||
@@ -28,7 +30,7 @@ import java.util.List;
 | 
			
		||||
public class JEIComputerCraft implements IModPlugin {
 | 
			
		||||
    @Override
 | 
			
		||||
    public ResourceLocation getPluginUid() {
 | 
			
		||||
        return new ResourceLocation(ComputerCraftAPI.MOD_ID, "jei");
 | 
			
		||||
        return ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "jei");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -44,7 +46,7 @@ public class JEIComputerCraft implements IModPlugin {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void registerAdvanced(IAdvancedRegistration registry) {
 | 
			
		||||
        registry.addRecipeManagerPlugin(new RecipeResolver());
 | 
			
		||||
        registry.addRecipeManagerPlugin(new RecipeResolver(getRegistryAccess()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -52,7 +54,7 @@ public class JEIComputerCraft implements IModPlugin {
 | 
			
		||||
        var registry = runtime.getRecipeManager();
 | 
			
		||||
 | 
			
		||||
        // Register all turtles/pocket computers (not just vanilla upgrades) as upgrades on JEI.
 | 
			
		||||
        var upgradeItems = RecipeModHelpers.getExtraStacks(RecipeModHelpers.getEmptyRegistryAccess());
 | 
			
		||||
        var upgradeItems = RecipeModHelpers.getExtraStacks(getRegistryAccess());
 | 
			
		||||
        if (!upgradeItems.isEmpty()) {
 | 
			
		||||
            runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, upgradeItems);
 | 
			
		||||
        }
 | 
			
		||||
@@ -99,4 +101,8 @@ public class JEIComputerCraft implements IModPlugin {
 | 
			
		||||
     * Distinguishes disks by colour.
 | 
			
		||||
     */
 | 
			
		||||
    private static final IIngredientSubtypeInterpreter<ItemStack> diskSubtype = (stack, ctx) -> Integer.toString(DiskItem.getColour(stack));
 | 
			
		||||
 | 
			
		||||
    private static RegistryAccess getRegistryAccess() {
 | 
			
		||||
        return Minecraft.getInstance().level.registryAccess();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,10 +2,9 @@
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.shared.integration.jei;
 | 
			
		||||
package dan200.computercraft.client.integration.jei;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.integration.RecipeModHelpers;
 | 
			
		||||
import dan200.computercraft.shared.integration.UpgradeRecipeGenerator;
 | 
			
		||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
 | 
			
		||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
 | 
			
		||||
@@ -14,6 +13,7 @@ import mezz.jei.api.recipe.IFocus;
 | 
			
		||||
import mezz.jei.api.recipe.RecipeType;
 | 
			
		||||
import mezz.jei.api.recipe.advanced.IRecipeManagerPlugin;
 | 
			
		||||
import mezz.jei.api.recipe.category.IRecipeCategory;
 | 
			
		||||
import net.minecraft.core.HolderLookup;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.world.item.ItemStack;
 | 
			
		||||
import net.minecraft.world.item.crafting.CraftingRecipe;
 | 
			
		||||
@@ -22,8 +22,19 @@ import net.minecraft.world.item.crafting.RecipeHolder;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
class RecipeResolver implements IRecipeManagerPlugin {
 | 
			
		||||
    private static final ResourceLocation RECIPE_ID = new ResourceLocation(ComputerCraftAPI.MOD_ID, "upgrade");
 | 
			
		||||
    private final UpgradeRecipeGenerator<RecipeHolder<CraftingRecipe>> resolver = new UpgradeRecipeGenerator<>(x -> new RecipeHolder<>(RECIPE_ID, x), RecipeModHelpers.getEmptyRegistryAccess());
 | 
			
		||||
    private final UpgradeRecipeGenerator<RecipeHolder<CraftingRecipe>> resolver;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * We need to generate unique ids for each recipe, as JEI will attempt to deduplicate them otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    private int nextId = 0;
 | 
			
		||||
 | 
			
		||||
    RecipeResolver(HolderLookup.Provider registries) {
 | 
			
		||||
        resolver = new UpgradeRecipeGenerator<>(
 | 
			
		||||
            x -> new RecipeHolder<>(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "upgrade_" + nextId++), x),
 | 
			
		||||
            registries
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public <V> List<RecipeType<?>> getRecipeTypes(IFocus<V> focus) {
 | 
			
		||||
@@ -60,7 +71,7 @@ class RecipeResolver implements IRecipeManagerPlugin {
 | 
			
		||||
        return List.of();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings({ "unchecked", "rawtypes" })
 | 
			
		||||
    @SuppressWarnings({ "unchecked", "rawtypes", "UnusedVariable" })
 | 
			
		||||
    private static <T, U> List<T> cast(RecipeType<U> ignoredType, List<U> from) {
 | 
			
		||||
        return (List) from;
 | 
			
		||||
    }
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.model;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.Gson;
 | 
			
		||||
import com.google.gson.JsonElement;
 | 
			
		||||
import com.google.gson.JsonParseException;
 | 
			
		||||
import com.mojang.serialization.Codec;
 | 
			
		||||
import com.mojang.serialization.JsonOps;
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.resources.ResourceManager;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A list of extra models to load on the client.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is largely intended for use with {@linkplain TurtleOverlay turtle overlays}. As overlays are stored in a dynamic
 | 
			
		||||
 * registry, they are not available when resources are loaded, and so we need a way to request the overlays' models be
 | 
			
		||||
 * loaded.
 | 
			
		||||
 *
 | 
			
		||||
 * @param models The models to load.
 | 
			
		||||
 */
 | 
			
		||||
public record ExtraModels(List<ResourceLocation> models) {
 | 
			
		||||
    private static final Logger LOG = LoggerFactory.getLogger(ExtraModels.class);
 | 
			
		||||
    private static final Gson GSON = new Gson();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The path where the extra models are listed.
 | 
			
		||||
     */
 | 
			
		||||
    public static final ResourceLocation PATH = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "extra_models.json");
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The coded used to store the extra model file.
 | 
			
		||||
     */
 | 
			
		||||
    public static final Codec<ExtraModels> CODEC = ResourceLocation.CODEC.listOf().xmap(ExtraModels::new, ExtraModels::models);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the list of all extra models to load.
 | 
			
		||||
     *
 | 
			
		||||
     * @param resources The current resource manager.
 | 
			
		||||
     * @return A set of all resources to load.
 | 
			
		||||
     */
 | 
			
		||||
    public static Collection<ResourceLocation> loadAll(ResourceManager resources) {
 | 
			
		||||
        Set<ResourceLocation> out = new HashSet<>();
 | 
			
		||||
 | 
			
		||||
        for (var path : resources.getResourceStack(PATH)) {
 | 
			
		||||
            ExtraModels models;
 | 
			
		||||
            try (var stream = path.openAsReader()) {
 | 
			
		||||
                models = ExtraModels.CODEC.parse(JsonOps.INSTANCE, GSON.fromJson(stream, JsonElement.class)).getOrThrow(JsonParseException::new);
 | 
			
		||||
            } catch (IOException | RuntimeException e) {
 | 
			
		||||
                LOG.error("Failed to load extra models from {}", path.sourcePackId());
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            out.addAll(models.models());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Collections.unmodifiableCollection(out);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,11 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.model.turtle;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormatElement;
 | 
			
		||||
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;
 | 
			
		||||
@@ -29,8 +27,8 @@ import java.util.List;
 | 
			
		||||
public class ModelTransformer {
 | 
			
		||||
    private static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
 | 
			
		||||
 | 
			
		||||
    private static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
 | 
			
		||||
    private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
 | 
			
		||||
    private static final int STRIDE = FaceBakery.VERTEX_INT_SIZE;
 | 
			
		||||
    private static final int POS_OFFSET = 0;
 | 
			
		||||
 | 
			
		||||
    protected final Matrix4f transformation;
 | 
			
		||||
    protected final boolean invert;
 | 
			
		||||
@@ -91,13 +89,4 @@ public class ModelTransformer {
 | 
			
		||||
 | 
			
		||||
    private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int findOffset(VertexFormat format, VertexFormatElement element) {
 | 
			
		||||
        var offset = 0;
 | 
			
		||||
        for (var other : format.getElements()) {
 | 
			
		||||
            if (other == element) return offset / Integer.BYTES;
 | 
			
		||||
            offset += element.getByteSize();
 | 
			
		||||
        }
 | 
			
		||||
        throw new IllegalArgumentException("Cannot find " + element + " in " + format);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,14 @@ 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.render.TurtleBlockEntityRenderer;
 | 
			
		||||
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;
 | 
			
		||||
@@ -52,7 +53,7 @@ public final class TurtleModelParts<T> {
 | 
			
		||||
        boolean colour,
 | 
			
		||||
        @Nullable UpgradeData<ITurtleUpgrade> leftUpgrade,
 | 
			
		||||
        @Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
 | 
			
		||||
        @Nullable ResourceLocation overlay,
 | 
			
		||||
        @Nullable TurtleOverlay overlay,
 | 
			
		||||
        boolean christmas,
 | 
			
		||||
        boolean flip
 | 
			
		||||
    ) {
 | 
			
		||||
@@ -85,7 +86,7 @@ public final class TurtleModelParts<T> {
 | 
			
		||||
    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.getModel(), x.getMatrix());
 | 
			
		||||
        this.transformer = x -> transformer.transform(x.model(), x.matrix());
 | 
			
		||||
        buildModel = x -> combineModel.apply(buildModel(x));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -113,10 +114,10 @@ public final class TurtleModelParts<T> {
 | 
			
		||||
        var parts = new ArrayList<BakedModel>(4);
 | 
			
		||||
        parts.add(transform(combo.colour() ? colourModel : familyModel, transformation));
 | 
			
		||||
 | 
			
		||||
        var overlayModelLocation = TurtleBlockEntityRenderer.getTurtleOverlayModel(combo.overlay(), combo.christmas());
 | 
			
		||||
        if (overlayModelLocation != null) {
 | 
			
		||||
            parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, overlayModelLocation), 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());
 | 
			
		||||
@@ -124,10 +125,14 @@ public final class TurtleModelParts<T> {
 | 
			
		||||
        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.getModel(), transformation.compose(model.getMatrix())));
 | 
			
		||||
        parts.add(transform(model.model(), transformation.compose(model.matrix())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private BakedModel transform(BakedModel model, Transformation transformation) {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,11 @@ import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
 | 
			
		||||
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.network.chat.Component;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
import net.minecraft.world.entity.player.Player;
 | 
			
		||||
import net.minecraft.world.item.JukeboxSong;
 | 
			
		||||
import net.minecraft.world.level.Level;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -60,10 +61,12 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name) {
 | 
			
		||||
        var mc = Minecraft.getInstance();
 | 
			
		||||
        ClientPlatformHelper.get().playStreamingMusic(pos, sound);
 | 
			
		||||
        if (name != null) mc.gui.setNowPlaying(Component.literal(name));
 | 
			
		||||
    public void handlePlayRecord(BlockPos pos, @Nullable Holder<JukeboxSong> song) {
 | 
			
		||||
        if (song == null) {
 | 
			
		||||
            Minecraft.getInstance().levelRenderer.stopJukeboxSongAndNotifyNearby(pos);
 | 
			
		||||
        } else {
 | 
			
		||||
            Minecraft.getInstance().levelRenderer.playJukeboxSong(song, pos);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,6 @@ 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 net.minecraft.core.BlockPos;
 | 
			
		||||
import net.minecraft.sounds.SoundEvent;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
@@ -28,13 +26,4 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
 | 
			
		||||
     * @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);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Play a record at a particular position.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pos   The position to play this record.
 | 
			
		||||
     * @param sound The record to play, or {@code null} to stop it.
 | 
			
		||||
     * @see net.minecraft.client.renderer.LevelRenderer#playStreamingMusic(SoundEvent, BlockPos)
 | 
			
		||||
     */
 | 
			
		||||
    void playStreamingMusic(BlockPos pos, @Nullable SoundEvent sound);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -62,15 +62,13 @@ public final class CableHighlightRenderer {
 | 
			
		||||
            zDelta = zDelta / len;
 | 
			
		||||
 | 
			
		||||
            buffer
 | 
			
		||||
                .vertex(matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset))
 | 
			
		||||
                .color(0, 0, 0, 0.4f)
 | 
			
		||||
                .normal(transform.last(), xDelta, yDelta, zDelta)
 | 
			
		||||
                .endVertex();
 | 
			
		||||
                .addVertex(matrix4f, (float) (x1 + xOffset), (float) (y1 + yOffset), (float) (z1 + zOffset))
 | 
			
		||||
                .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
                .setNormal(transform.last(), xDelta, yDelta, zDelta);
 | 
			
		||||
            buffer
 | 
			
		||||
                .vertex(matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset))
 | 
			
		||||
                .color(0, 0, 0, 0.4f)
 | 
			
		||||
                .normal(transform.last(), xDelta, yDelta, zDelta)
 | 
			
		||||
                .endVertex();
 | 
			
		||||
                .addVertex(matrix4f, (float) (x2 + xOffset), (float) (y2 + yOffset), (float) (z2 + zOffset))
 | 
			
		||||
                .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
                .setNormal(transform.last(), xDelta, yDelta, zDelta);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -50,28 +50,23 @@ public final class ModelRenderer {
 | 
			
		||||
                if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var r = (float) (tint >> 16 & 255) / 255.0F;
 | 
			
		||||
            var g = (float) (tint >> 8 & 255) / 255.0F;
 | 
			
		||||
            var b = (float) (tint & 255) / 255.0F;
 | 
			
		||||
            putBulkQuad(buffer, matrix, bakedquad, r, g, b, lightmapCoord, overlayLight, inverted);
 | 
			
		||||
            putBulkQuad(buffer, matrix, bakedquad, tint, lightmapCoord, overlayLight, inverted);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, int, int)} which
 | 
			
		||||
     * 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 red           The red tint of this quad.
 | 
			
		||||
     * @param green         The  green tint of this quad.
 | 
			
		||||
     * @param blue          The blue tint of this quad.
 | 
			
		||||
     * @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, float red, float green, float blue, int lightmapCoord, int overlayLight, boolean invert) {
 | 
			
		||||
    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.
 | 
			
		||||
@@ -93,9 +88,9 @@ public final class ModelRenderer {
 | 
			
		||||
 | 
			
		||||
            var u = Float.intBitsToFloat(vertices[i + 4]);
 | 
			
		||||
            var v = Float.intBitsToFloat(vertices[i + 5]);
 | 
			
		||||
            buffer.vertex(
 | 
			
		||||
            buffer.addVertex(
 | 
			
		||||
                vector.x(), vector.y(), vector.z(),
 | 
			
		||||
                red, green, blue, 1.0F, u, v, overlayLight, lightmapCoord,
 | 
			
		||||
                colour, u, v, overlayLight, lightmapCoord,
 | 
			
		||||
                normalX, normalY, normalZ
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -158,7 +158,7 @@ public final class PrintoutRenderer {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void vertex(VertexConsumer buffer, Matrix4f matrix, float x, float y, float z, float u, float v, int light) {
 | 
			
		||||
        buffer.vertex(matrix, x, y, z).color(255, 255, 255, 255).uv(u, v).uv2(light).endVertex();
 | 
			
		||||
        buffer.addVertex(matrix, x, y, z).setColor(255, 255, 255, 255).setUv(u, v).setLight(light);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static float offsetAt(int page) {
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ public class RenderTypes {
 | 
			
		||||
     * 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(new ResourceLocation("computercraft", "textures/gui/printout.png"));
 | 
			
		||||
    public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/printout.png"));
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Render type for {@linkplain GuiSprites GUI sprites}.
 | 
			
		||||
 
 | 
			
		||||
@@ -118,10 +118,10 @@ public class SpriteRenderer {
 | 
			
		||||
     */
 | 
			
		||||
    public void blit(
 | 
			
		||||
        int x, int y, int width, int height, float u0, float v0, float u1, float v1) {
 | 
			
		||||
        builder.vertex(transform, x, y + height, z).color(r, g, b, 255).uv(u0, v1).uv2(light).endVertex();
 | 
			
		||||
        builder.vertex(transform, x + width, y + height, z).color(r, g, b, 255).uv(u1, v1).uv2(light).endVertex();
 | 
			
		||||
        builder.vertex(transform, x + width, y, z).color(r, g, b, 255).uv(u1, v0).uv2(light).endVertex();
 | 
			
		||||
        builder.vertex(transform, x, y, z).color(r, g, b, 255).uv(u0, v0).uv2(light).endVertex();
 | 
			
		||||
        builder.addVertex(transform, x, y + height, z).setColor(r, g, b, 255).setUv(u0, v1).setLight(light);
 | 
			
		||||
        builder.addVertex(transform, x + width, y + height, z).setColor(r, g, b, 255).setUv(u1, v1).setLight(light);
 | 
			
		||||
        builder.addVertex(transform, x + width, y, z).setColor(r, g, b, 255).setUv(u1, v0).setLight(light);
 | 
			
		||||
        builder.addVertex(transform, x, y, z).setColor(r, g, b, 255).setUv(u0, v0).setLight(light);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static float u(TextureAtlasSprite sprite, int x, int width) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
 | 
			
		||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
 | 
			
		||||
import dan200.computercraft.shared.util.Holiday;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
@@ -21,14 +22,14 @@ 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.CommonColors;
 | 
			
		||||
import net.minecraft.world.phys.BlockHitResult;
 | 
			
		||||
import net.minecraft.world.phys.HitResult;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
 | 
			
		||||
    private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
 | 
			
		||||
    private static final ResourceLocation ELF_OVERLAY_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
 | 
			
		||||
    public static final ResourceLocation COLOUR_TURTLE_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
 | 
			
		||||
 | 
			
		||||
    private final BlockEntityRenderDispatcher renderer;
 | 
			
		||||
    private final Font font;
 | 
			
		||||
@@ -38,12 +39,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        font = context.getFont();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static @Nullable ResourceLocation getTurtleOverlayModel(@Nullable ResourceLocation overlay, boolean christmas) {
 | 
			
		||||
        if (overlay != null) return overlay;
 | 
			
		||||
        if (christmas) return ELF_OVERLAY_MODEL;
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight) {
 | 
			
		||||
        transform.pushPose();
 | 
			
		||||
@@ -62,13 +57,13 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
            transform.pushPose();
 | 
			
		||||
            transform.translate(0.5, 1.2, 0.5);
 | 
			
		||||
            transform.mulPose(mc.getEntityRenderDispatcher().cameraOrientation());
 | 
			
		||||
            transform.scale(-0.025f, -0.025f, 0.025f);
 | 
			
		||||
            transform.scale(0.025f, -0.025f, 0.025f);
 | 
			
		||||
 | 
			
		||||
            var matrix = transform.last().pose();
 | 
			
		||||
            var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
 | 
			
		||||
            var width = -font.width(label) / 2.0f;
 | 
			
		||||
            font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
 | 
			
		||||
            font.drawInBatch(label, width, (float) 0, 0xffffffff, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
 | 
			
		||||
            font.drawInBatch(label, width, (float) 0, CommonColors.WHITE, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
 | 
			
		||||
 | 
			
		||||
            transform.popPose();
 | 
			
		||||
        }
 | 
			
		||||
@@ -98,10 +93,11 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Render the overlay
 | 
			
		||||
        var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
 | 
			
		||||
        if (overlayModel != null) {
 | 
			
		||||
            renderModel(transform, buffers, lightmapCoord, overlayLight, overlayModel, null);
 | 
			
		||||
        }
 | 
			
		||||
        if (overlay != null) renderModel(transform, buffers, lightmapCoord, overlayLight, overlay.model(), null);
 | 
			
		||||
 | 
			
		||||
        // And the Christmas overlay.
 | 
			
		||||
        var showChristmas = TurtleOverlay.showElfOverlay(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
 | 
			
		||||
        if (showChristmas) renderModel(transform, buffers, lightmapCoord, overlayLight, TurtleOverlay.ELF_MODEL, null);
 | 
			
		||||
 | 
			
		||||
        // Render the upgrades
 | 
			
		||||
        renderUpgrade(transform, buffers, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks);
 | 
			
		||||
@@ -121,8 +117,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
 | 
			
		||||
        transform.translate(0.0f, -0.5f, -0.5f);
 | 
			
		||||
 | 
			
		||||
        var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
 | 
			
		||||
        applyTransformation(transform, model.getMatrix());
 | 
			
		||||
        renderModel(transform, buffers, lightmapCoord, overlayLight, model.getModel(), null);
 | 
			
		||||
        applyTransformation(transform, model.matrix());
 | 
			
		||||
        renderModel(transform, buffers, lightmapCoord, overlayLight, model.model(), null);
 | 
			
		||||
 | 
			
		||||
        transform.popPose();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,11 @@
 | 
			
		||||
package dan200.computercraft.client.render.monitor;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.platform.GlStateManager;
 | 
			
		||||
import com.mojang.blaze3d.platform.MemoryTracker;
 | 
			
		||||
import com.mojang.blaze3d.systems.RenderSystem;
 | 
			
		||||
import com.mojang.blaze3d.vertex.*;
 | 
			
		||||
import com.mojang.blaze3d.vertex.PoseStack;
 | 
			
		||||
import com.mojang.blaze3d.vertex.Tesselator;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexBuffer;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import com.mojang.math.Axis;
 | 
			
		||||
import dan200.computercraft.annotations.ForgeOverride;
 | 
			
		||||
import dan200.computercraft.client.FrameInfo;
 | 
			
		||||
@@ -18,6 +20,7 @@ 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;
 | 
			
		||||
@@ -27,11 +30,11 @@ 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.Matrix3f;
 | 
			
		||||
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 javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
@@ -48,8 +51,6 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
     */
 | 
			
		||||
    private static final float MARGIN = (float) (MonitorBlockEntity.RENDER_MARGIN * 1.1);
 | 
			
		||||
 | 
			
		||||
    private static final Matrix3f IDENTITY_NORMAL = new Matrix3f().identity();
 | 
			
		||||
 | 
			
		||||
    private static @Nullable ByteBuffer backingBuffer;
 | 
			
		||||
 | 
			
		||||
    private static long lastFrame = -1;
 | 
			
		||||
@@ -163,13 +164,12 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
                var shader = RenderTypes.getMonitorTextureBufferShader();
 | 
			
		||||
                shader.setupUniform(renderState.tboUniform);
 | 
			
		||||
 | 
			
		||||
                var buffer = Tesselator.getInstance().getBuilder();
 | 
			
		||||
                buffer.begin(RenderTypes.MONITOR_TBO.mode(), RenderTypes.MONITOR_TBO.format());
 | 
			
		||||
                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.end(buffer, VertexSorting.DISTANCE_TO_ORIGIN);
 | 
			
		||||
                RenderTypes.MONITOR_TBO.draw(Nullability.assertNonNull(buffer.build()));
 | 
			
		||||
            }
 | 
			
		||||
            case VBO -> {
 | 
			
		||||
                var backgroundBuffer = assertNonNull(renderState.backgroundBuffer);
 | 
			
		||||
@@ -245,13 +245,13 @@ public class MonitorBlockEntityRenderer implements BlockEntityRenderer<MonitorBl
 | 
			
		||||
 | 
			
		||||
    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.vertex(matrix, x, y, 0).uv(x, y).endVertex();
 | 
			
		||||
        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 ? MemoryTracker.create(capacity) : MemoryTracker.resize(buffer, capacity);
 | 
			
		||||
            buffer = backingBuffer = buffer == null ? MemoryUtil.memAlloc(capacity) : MemoryUtil.memRealloc(buffer, capacity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        buffer.clear();
 | 
			
		||||
 
 | 
			
		||||
@@ -72,18 +72,16 @@ public final class MonitorHighlightRenderer {
 | 
			
		||||
 | 
			
		||||
    private static void line(VertexConsumer buffer, Matrix4f transform, PoseStack.Pose normal, float x, float y, float z, Direction direction) {
 | 
			
		||||
        buffer
 | 
			
		||||
            .vertex(transform, x, y, z)
 | 
			
		||||
            .color(0, 0, 0, 0.4f)
 | 
			
		||||
            .normal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ())
 | 
			
		||||
            .endVertex();
 | 
			
		||||
            .addVertex(transform, x, y, z)
 | 
			
		||||
            .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
            .setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
 | 
			
		||||
        buffer
 | 
			
		||||
            .vertex(transform,
 | 
			
		||||
            .addVertex(transform,
 | 
			
		||||
                x + direction.getStepX(),
 | 
			
		||||
                y + direction.getStepY(),
 | 
			
		||||
                z + direction.getStepZ()
 | 
			
		||||
            )
 | 
			
		||||
            .color(0, 0, 0, 0.4f)
 | 
			
		||||
            .normal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ())
 | 
			
		||||
            .endVertex();
 | 
			
		||||
            .setColor(0, 0, 0, 0.4f)
 | 
			
		||||
            .setNormal(normal, direction.getStepX(), direction.getStepY(), direction.getStepZ());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.client.render.text;
 | 
			
		||||
 | 
			
		||||
import com.mojang.blaze3d.platform.MemoryTracker;
 | 
			
		||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexConsumer;
 | 
			
		||||
import com.mojang.blaze3d.vertex.VertexFormat;
 | 
			
		||||
@@ -28,7 +27,7 @@ import static org.lwjgl.system.MemoryUtil.*;
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>No transformation matrix (not needed for VBOs).</li>
 | 
			
		||||
 *   <li>Only works with {@link DefaultVertexFormat#POSITION_COLOR_TEX_LIGHTMAP}.</li>
 | 
			
		||||
 *   <li>The buffer <strong>MUST</strong> be allocated with {@link MemoryTracker}, and not through any other means.</li>
 | 
			
		||||
 *   <li>The buffer <strong>MUST</strong> be allocated with {@link MemoryUtil}, and not through any other means.</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Note this is almost an exact copy of {@link FixedWidthFontRenderer}. While the code duplication is unfortunate,
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMA
 | 
			
		||||
 * {@link DirectFixedWidthFontRenderer}.
 | 
			
		||||
 */
 | 
			
		||||
public final class FixedWidthFontRenderer {
 | 
			
		||||
    public static final ResourceLocation FONT = new ResourceLocation("computercraft", "textures/gui/term_font.png");
 | 
			
		||||
    public static final ResourceLocation FONT = ResourceLocation.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");
 | 
			
		||||
 | 
			
		||||
    public static final int FONT_HEIGHT = 9;
 | 
			
		||||
    public static final int FONT_WIDTH = 6;
 | 
			
		||||
@@ -221,9 +221,9 @@ public final class FixedWidthFontRenderer {
 | 
			
		||||
        var consumer = c.consumer();
 | 
			
		||||
        byte r = rgba[0], g = rgba[1], b = rgba[2], a = rgba[3];
 | 
			
		||||
 | 
			
		||||
        consumer.vertex(poseMatrix, x1, y1, z).color(r, g, b, a).uv(u1, v1).uv2(light).endVertex();
 | 
			
		||||
        consumer.vertex(poseMatrix, x1, y2, z).color(r, g, b, a).uv(u1, v2).uv2(light).endVertex();
 | 
			
		||||
        consumer.vertex(poseMatrix, x2, y2, z).color(r, g, b, a).uv(u2, v2).uv2(light).endVertex();
 | 
			
		||||
        consumer.vertex(poseMatrix, x2, y1, z).color(r, g, b, a).uv(u2, v1).uv2(light).endVertex();
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import javax.annotation.Nullable;
 | 
			
		||||
 * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
 | 
			
		||||
 */
 | 
			
		||||
public class SpeakerInstance {
 | 
			
		||||
    public static final ResourceLocation DFPWM_STREAM = new ResourceLocation(ComputerCraftAPI.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played");
 | 
			
		||||
    public static final ResourceLocation DFPWM_STREAM = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "speaker.dfpwm_fake_audio_should_not_be_played");
 | 
			
		||||
 | 
			
		||||
    private @Nullable DfpwmStream currentStream;
 | 
			
		||||
    private @Nullable SpeakerSound sound;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,14 @@
 | 
			
		||||
package dan200.computercraft.client.turtle;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.ComputerCraftAPI;
 | 
			
		||||
import dan200.computercraft.api.client.ModelLocation;
 | 
			
		||||
import dan200.computercraft.api.client.TransformedModel;
 | 
			
		||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.shared.ModRegistry;
 | 
			
		||||
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
 | 
			
		||||
import dan200.computercraft.shared.util.DataComponentUtil;
 | 
			
		||||
import net.minecraft.core.component.DataComponentPatch;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
@@ -23,8 +25,7 @@ import java.util.stream.Stream;
 | 
			
		||||
public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public TransformedModel getModel(TurtleModem upgrade, @Nullable ITurtleAccess turtle, TurtleSide side, DataComponentPatch data) {
 | 
			
		||||
        var component = data.get(ModRegistry.DataComponents.ON.get());
 | 
			
		||||
        var active = component != null && component.isPresent() && component.get();
 | 
			
		||||
        var active = DataComponentUtil.isPresent(data, ModRegistry.DataComponents.ON.get(), x -> x);
 | 
			
		||||
 | 
			
		||||
        var models = upgrade.advanced() ? ModemModels.ADVANCED : ModemModels.NORMAL;
 | 
			
		||||
        return side == TurtleSide.LEFT
 | 
			
		||||
@@ -38,23 +39,23 @@ public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private record ModemModels(
 | 
			
		||||
        ResourceLocation leftOffModel, ResourceLocation rightOffModel,
 | 
			
		||||
        ResourceLocation leftOnModel, ResourceLocation rightOnModel
 | 
			
		||||
        ModelLocation leftOffModel, ModelLocation rightOffModel,
 | 
			
		||||
        ModelLocation leftOnModel, ModelLocation rightOnModel
 | 
			
		||||
    ) {
 | 
			
		||||
        private static final ModemModels NORMAL = create("normal");
 | 
			
		||||
        private static final ModemModels ADVANCED = create("advanced");
 | 
			
		||||
 | 
			
		||||
        public static ModemModels create(String type) {
 | 
			
		||||
            return new ModemModels(
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left"),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right"),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left"),
 | 
			
		||||
                new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right")
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left")),
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right")),
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left")),
 | 
			
		||||
                ModelLocation.ofResource(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right"))
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
            return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel);
 | 
			
		||||
            return Stream.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel).flatMap(ModelLocation::getDependencies);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,12 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
 | 
			
		||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
 | 
			
		||||
import dan200.computercraft.api.turtle.TurtleSide;
 | 
			
		||||
import dan200.computercraft.api.upgrades.UpgradeType;
 | 
			
		||||
import dan200.computercraft.impl.RegistryHelper;
 | 
			
		||||
import dan200.computercraft.impl.UpgradeManager;
 | 
			
		||||
import dan200.computercraft.shared.util.RegistryHelper;
 | 
			
		||||
import net.minecraft.client.Minecraft;
 | 
			
		||||
import net.minecraft.core.component.DataComponentPatch;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.WeakHashMap;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
@@ -32,14 +30,6 @@ public final class TurtleUpgradeModellers {
 | 
			
		||||
    private static final Map<UpgradeType<? extends ITurtleUpgrade>, TurtleUpgradeModeller<?>> turtleModels = new ConcurrentHashMap<>();
 | 
			
		||||
    private static volatile boolean fetchedModels;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * In order to avoid a double lookup of {@link ITurtleUpgrade} to {@link UpgradeManager.UpgradeWrapper} to
 | 
			
		||||
     * {@link TurtleUpgradeModeller}, we maintain a cache here.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Turtle upgrades may be removed as part of datapack reloads, so we use a weak map to avoid the memory leak.
 | 
			
		||||
     */
 | 
			
		||||
    private static final WeakHashMap<ITurtleUpgrade, TurtleUpgradeModeller<?>> modelCache = new WeakHashMap<>();
 | 
			
		||||
 | 
			
		||||
    private TurtleUpgradeModellers() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -57,20 +47,17 @@ public final class TurtleUpgradeModellers {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TransformedModel getModel(ITurtleUpgrade upgrade, ITurtleAccess access, TurtleSide side) {
 | 
			
		||||
        @SuppressWarnings("unchecked")
 | 
			
		||||
        var modeller = (TurtleUpgradeModeller<ITurtleUpgrade>) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller);
 | 
			
		||||
        return modeller.getModel(upgrade, access, side, access.getUpgradeData(side));
 | 
			
		||||
        return getModeller(upgrade).getModel(upgrade, access, side, access.getUpgradeData(side));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static TransformedModel getModel(ITurtleUpgrade upgrade, DataComponentPatch data, TurtleSide side) {
 | 
			
		||||
        @SuppressWarnings("unchecked")
 | 
			
		||||
        var modeller = (TurtleUpgradeModeller<ITurtleUpgrade>) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller);
 | 
			
		||||
        return modeller.getModel(upgrade, null, side, data);
 | 
			
		||||
        return getModeller(upgrade).getModel(upgrade, null, side, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static TurtleUpgradeModeller<?> getModeller(ITurtleUpgrade upgrade) {
 | 
			
		||||
    @SuppressWarnings("unchecked")
 | 
			
		||||
    private static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> getModeller(T upgrade) {
 | 
			
		||||
        var modeller = turtleModels.get(upgrade.getType());
 | 
			
		||||
        return modeller == null ? NULL_TURTLE_MODELLER : modeller;
 | 
			
		||||
        return (TurtleUpgradeModeller<T>) (modeller == null ? NULL_TURTLE_MODELLER : modeller);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Stream<ResourceLocation> getDependencies() {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,15 +6,18 @@ package dan200.computercraft.data.client;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.client.gui.GuiSprites;
 | 
			
		||||
import dan200.computercraft.data.DataProviders;
 | 
			
		||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
 | 
			
		||||
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
 | 
			
		||||
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
 | 
			
		||||
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
 | 
			
		||||
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
 | 
			
		||||
import net.minecraft.core.HolderLookup;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
import net.minecraft.server.packs.PackType;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -26,9 +29,9 @@ public final class ClientDataProviders {
 | 
			
		||||
    private ClientDataProviders() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void add(DataProviders.GeneratorSink generator) {
 | 
			
		||||
    public static void add(DataProviders.GeneratorSink generator, CompletableFuture<HolderLookup.Provider> registries) {
 | 
			
		||||
        generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
 | 
			
		||||
            out.accept(new ResourceLocation("blocks"), List.of(
 | 
			
		||||
            out.accept(ResourceLocation.withDefaultNamespace("blocks"), List.of(
 | 
			
		||||
                new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
 | 
			
		||||
                new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty())
 | 
			
		||||
            ));
 | 
			
		||||
@@ -44,5 +47,12 @@ public final class ClientDataProviders {
 | 
			
		||||
                GuiSprites.COMPUTER_COLOUR.textures()
 | 
			
		||||
            ).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList());
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        generator.add(pack -> new ExtraModelsProvider(pack, registries) {
 | 
			
		||||
            @Override
 | 
			
		||||
            public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
 | 
			
		||||
                return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.data.client;
 | 
			
		||||
 | 
			
		||||
import com.mojang.serialization.JsonOps;
 | 
			
		||||
import dan200.computercraft.client.model.ExtraModels;
 | 
			
		||||
import net.minecraft.core.HolderLookup;
 | 
			
		||||
import net.minecraft.data.CachedOutput;
 | 
			
		||||
import net.minecraft.data.DataProvider;
 | 
			
		||||
import net.minecraft.data.PackOutput;
 | 
			
		||||
import net.minecraft.resources.ResourceLocation;
 | 
			
		||||
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A data provider to generate {@link ExtraModels}.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ExtraModelsProvider implements DataProvider {
 | 
			
		||||
    private final Path path;
 | 
			
		||||
    private final CompletableFuture<HolderLookup.Provider> registries;
 | 
			
		||||
 | 
			
		||||
    ExtraModelsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
 | 
			
		||||
        path = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(ExtraModels.PATH.getNamespace()).resolve(ExtraModels.PATH.getPath());
 | 
			
		||||
        this.registries = registries;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a stream of models to load.
 | 
			
		||||
     *
 | 
			
		||||
     * @param registries The current registries.
 | 
			
		||||
     * @return The collection of extra models to load.
 | 
			
		||||
     */
 | 
			
		||||
    public abstract Stream<ResourceLocation> getModels(HolderLookup.Provider registries);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final CompletableFuture<?> run(CachedOutput output) {
 | 
			
		||||
        return registries.thenCompose(registries -> {
 | 
			
		||||
            var models = new ExtraModels(getModels(registries).sorted().toList());
 | 
			
		||||
            var json = ExtraModels.CODEC.encodeStart(JsonOps.INSTANCE, models).getOrThrow(IllegalStateException::new);
 | 
			
		||||
            return DataProvider.saveStable(output, json, path);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final String getName() {
 | 
			
		||||
        return "Extra Models";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								projects/common/src/generated/resources/assets/computercraft/extra_models.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								projects/common/src/generated/resources/assets/computercraft/extra_models.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
["computercraft:block/turtle_rainbow_overlay", "computercraft:block/turtle_trans_overlay"]
 | 
			
		||||
@@ -206,8 +206,10 @@
 | 
			
		||||
  "item.computercraft.treasure_disk": "Floppy Disk",
 | 
			
		||||
  "itemGroup.computercraft": "ComputerCraft",
 | 
			
		||||
  "tag.item.computercraft.computer": "Computers",
 | 
			
		||||
  "tag.item.computercraft.dyeable": "Dyable items",
 | 
			
		||||
  "tag.item.computercraft.monitor": "Monitors",
 | 
			
		||||
  "tag.item.computercraft.turtle": "Turtles",
 | 
			
		||||
  "tag.item.computercraft.turtle_can_place": "Turtle-placeable items",
 | 
			
		||||
  "tag.item.computercraft.wired_modem": "Wired modems",
 | 
			
		||||
  "tracking_field.computercraft.avg": "%s (avg)",
 | 
			
		||||
  "tracking_field.computercraft.computer_tasks.name": "Tasks",
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,12 @@
 | 
			
		||||
{"parent": "computercraft:block/turtle_base", "textures": {"texture": "computercraft:block/turtle_advanced"}}
 | 
			
		||||
{
 | 
			
		||||
  "parent": "computercraft:block/turtle_base",
 | 
			
		||||
  "textures": {
 | 
			
		||||
    "back": "computercraft:block/turtle_advanced_back",
 | 
			
		||||
    "backpack": "computercraft:block/turtle_advanced_backpack",
 | 
			
		||||
    "bottom": "computercraft:block/turtle_advanced_bottom",
 | 
			
		||||
    "front": "computercraft:block/turtle_advanced_front",
 | 
			
		||||
    "left": "computercraft:block/turtle_advanced_left",
 | 
			
		||||
    "right": "computercraft:block/turtle_advanced_right",
 | 
			
		||||
    "top": "computercraft:block/turtle_advanced_top"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,12 @@
 | 
			
		||||
{"parent": "computercraft:block/turtle_base", "textures": {"texture": "computercraft:block/turtle_normal"}}
 | 
			
		||||
{
 | 
			
		||||
  "parent": "computercraft:block/turtle_base",
 | 
			
		||||
  "textures": {
 | 
			
		||||
    "back": "computercraft:block/turtle_normal_back",
 | 
			
		||||
    "backpack": "computercraft:block/turtle_normal_backpack",
 | 
			
		||||
    "bottom": "computercraft:block/turtle_normal_bottom",
 | 
			
		||||
    "front": "computercraft:block/turtle_normal_front",
 | 
			
		||||
    "left": "computercraft:block/turtle_normal_left",
 | 
			
		||||
    "right": "computercraft:block/turtle_normal_right",
 | 
			
		||||
    "top": "computercraft:block/turtle_normal_top"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user