1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-06-03 07:04:11 +00:00

Merge branch 'mc-1.20.x' into mc-1.21.x

Oh, I'm sure I missed something here. This was a nasty merge, has the
docs have changed so much in each version.
This commit is contained in:
Jonathan Coates 2025-01-11 17:53:53 +00:00
commit 7337b91692
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
61 changed files with 1009 additions and 491 deletions

View File

@ -58,6 +58,7 @@ repos:
exclude: | exclude: |
(?x)^( (?x)^(
projects/[a-z]+/src/generated| projects/[a-z]+/src/generated|
projects/[a-z]+/src/examples/generatedResources|
projects/core/src/test/resources/test-rom/data/json-parsing/| projects/core/src/test/resources/test-rom/data/json-parsing/|
.*\.dfpwm .*\.dfpwm
) )

View File

@ -22,8 +22,7 @@ If you have a bug, suggestion, or other feedback, the best thing to do is [file
use the issue templates - they provide a useful hint on what information to provide. use the issue templates - they provide a useful hint on what information to provide.
## Translations ## Translations
Translations are managed through [CrowdIn], an online interface for managing language strings. Translations may either Translations are managed through [CrowdIn], an online interface for managing language strings.
be contributed there, or directly via a pull request.
## Setting up a development environment ## Setting up a development environment
In order to develop CC: Tweaked, you'll need to download the source code and then run it. In order to develop CC: Tweaked, you'll need to download the source code and then run it.
@ -49,9 +48,12 @@ If you want to run CC:T in a normal Minecraft instance, run `./gradlew assemble`
`projects/forge/build/libs` (for Forge) or `projects/fabric/build/libs` (for Fabric). `projects/forge/build/libs` (for Forge) or `projects/fabric/build/libs` (for Fabric).
## Developing CC: Tweaked ## Developing CC: Tweaked
Before making any major changes to CC: Tweaked, I'd recommend you have a read of the [the architecture Before making any major changes to CC: Tweaked, I'd recommend starting opening an issue or starting a discussion on
document][architecture] first. While it's not a comprehensive document, it gives a good hint of where you should start GitHub first. It's often helpful to discuss features before spending time developing them!
looking to make your changes. As always, if you're not sure, [do ask the community][community]!
Once you're ready to start programming, have a read of the [the architecture document][architecture] first. While it's
not a comprehensive document, it gives a good hint of where you should start looking to make your changes. As always, if
you're not sure, [do ask the community][community]!
### Testing ### Testing
When making larger changes, it may be useful to write a test to make sure your code works as expected. When making larger changes, it may be useful to write a test to make sure your code works as expected.

View File

@ -60,19 +60,6 @@ dependencies {
} }
``` ```
When using ForgeGradle, you may also need to add the following:
```groovy
minecraft {
runs {
configureEach {
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg"
}
}
}
```
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file
an issue to let me know! an issue to let me know!

View File

@ -8,10 +8,10 @@ SPDX-PackageSupplier = "Jonathan Coates <git@squiddev.cc>"
SPDX-PackageDownloadLocation = "https://github.com/cc-tweaked/cc-tweaked" SPDX-PackageDownloadLocation = "https://github.com/cc-tweaked/cc-tweaked"
[[annotations]] [[annotations]]
# Generated/data files are CC0.
SPDX-FileCopyrightText = "The CC: Tweaked Developers" SPDX-FileCopyrightText = "The CC: Tweaked Developers"
SPDX-License-Identifier = "CC0-1.0" SPDX-License-Identifier = "CC0-1.0"
path = [ path = [
# Generated/data files are CC0.
"gradle/gradle-daemon-jvm.properties", "gradle/gradle-daemon-jvm.properties",
"projects/common/src/main/resources/assets/computercraft/sounds.json", "projects/common/src/main/resources/assets/computercraft/sounds.json",
"projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg", "projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg",
@ -20,6 +20,11 @@ path = [
"projects/**/src/generated/**", "projects/**/src/generated/**",
"projects/web/src/htmlTransform/export/index.json", "projects/web/src/htmlTransform/export/index.json",
"projects/web/src/htmlTransform/export/items/minecraft/**", "projects/web/src/htmlTransform/export/items/minecraft/**",
# GitHub build scripts are CC0. While we could add a header to each file,
# it's unclear if it will break actions or issue templates in some way.
".github/**",
# Example mod is CC0.
"projects/**/src/examples/**"
] ]
[[annotations]] [[annotations]]
@ -74,7 +79,7 @@ path = [
] ]
[[annotations]] [[annotations]]
# Community-contributed license files # Community-contributed language files
SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers"
SPDX-License-Identifier = "LicenseRef-CCPL" SPDX-License-Identifier = "LicenseRef-CCPL"
path = [ path = [
@ -88,18 +93,11 @@ path = [
] ]
[[annotations]] [[annotations]]
# Community-contributed license files # Community-contributed language files
SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers"
SPDX-License-Identifier = "MPL-2.0" SPDX-License-Identifier = "MPL-2.0"
path = "projects/common/src/main/resources/assets/computercraft/lang/**" path = "projects/common/src/main/resources/assets/computercraft/lang/**"
[[annotations]]
# GitHub build scripts are CC0. While we could add a header to each file,
# it's unclear if it will break actions or issue templates in some way.
SPDX-FileCopyrightText = "Jonathan Coates <git@squiddev.cc>"
SPDX-License-Identifier = "CC0-1.0"
path = ".github/**"
[[annotations]] [[annotations]]
path = ["gradle/wrapper/**"] path = ["gradle/wrapper/**"]
SPDX-FileCopyrightText = "Gradle Inc" SPDX-FileCopyrightText = "Gradle Inc"

View File

@ -14,14 +14,10 @@ repositories {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven("https://maven.neoforged.net/releases") { maven("https://maven.neoforged.net") {
name = "NeoForge" name = "NeoForge"
content { content {
includeGroup("net.minecraftforge")
includeGroup("net.neoforged") includeGroup("net.neoforged")
includeGroup("net.neoforged.gradle")
includeModule("codechicken", "DiffPatch")
includeModule("net.covers1624", "Quack")
} }
} }
@ -48,7 +44,7 @@ dependencies {
implementation(libs.fabric.loom) implementation(libs.fabric.loom)
implementation(libs.ideaExt) implementation(libs.ideaExt)
implementation(libs.minotaur) implementation(libs.minotaur)
implementation(libs.neoGradle.userdev) implementation(libs.modDevGradle)
implementation(libs.vanillaExtract) implementation(libs.vanillaExtract)
} }

View File

@ -11,20 +11,18 @@ import cc.tweaked.gradle.MinecraftConfigurations
plugins { plugins {
id("cc-tweaked.java-convention") id("cc-tweaked.java-convention")
id("net.neoforged.gradle.userdev") id("net.neoforged.moddev")
} }
plugins.apply(CCTweakedPlugin::class.java) plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra val mcVersion: String by extra
minecraft { neoForge {
modIdentifier("computercraft") val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
} version = libs.findVersion("neoForge").get().toString()
subsystems {
parchment { parchment {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
minecraftVersion = libs.findVersion("parchmentMc").get().toString() minecraftVersion = libs.findVersion("parchmentMc").get().toString()
mappingsVersion = libs.findVersion("parchment").get().toString() mappingsVersion = libs.findVersion("parchment").get().toString()
} }

View File

@ -99,6 +99,7 @@ sourceSets.all {
check("OperatorPrecedence", CheckSeverity.OFF) // For now. check("OperatorPrecedence", CheckSeverity.OFF) // For now.
check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid
check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty
check("InvalidInlineTag", CheckSeverity.OFF) // Triggered by @snippet. Can be removed on Java 21.
check("NullAway", CheckSeverity.ERROR) check("NullAway", CheckSeverity.ERROR)
option( option(

View File

@ -2,15 +2,16 @@
// //
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
/** /**
* Sets up the configurations for writing game tests. * Sets up the configurations for writing game tests.
* *
* See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas. * See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas.
*/ */
import cc.tweaked.gradle.MinecraftConfigurations
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
plugins { plugins {
id("cc-tweaked.kotlin-convention") id("cc-tweaked.kotlin-convention")
id("cc-tweaked.java-convention") id("cc-tweaked.java-convention")
@ -19,33 +20,16 @@ plugins {
val main = sourceSets["main"] val main = sourceSets["main"]
val client = sourceSets["client"] val client = sourceSets["client"]
// datagen and testMod inherit from the main and client classpath, just so we have access to Minecraft classes. MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.DATAGEN)
val datagen by sourceSets.creating { MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.EXAMPLES)
compileClasspath += main.compileClasspath + client.compileClasspath MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.TEST_MOD)
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
}
val testMod by sourceSets.creating { // Set up generated resources
compileClasspath += main.compileClasspath + client.compileClasspath sourceSets.main { resources.srcDir("src/generated/resources") }
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath sourceSets.named("examples") { resources.srcDir("src/examples/generatedResources") }
}
val extraConfigurations = listOf(datagen, testMod) // Make sure our examples compile.
tasks.check { dependsOn(tasks.named("compileExamplesJava")) }
configurations {
for (config in extraConfigurations) {
named(config.compileClasspathConfigurationName) { shouldResolveConsistentlyWith(compileClasspath.get()) }
named(config.runtimeClasspathConfigurationName) { shouldResolveConsistentlyWith(runtimeClasspath.get()) }
}
}
// Like the main test configurations, we're safe to depend on source set outputs.
dependencies {
for (config in extraConfigurations) {
add(config.implementationConfigurationName, main.output)
add(config.implementationConfigurationName, client.output)
}
}
// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath. // Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath.

View File

@ -113,7 +113,7 @@ abstract class CCTweakedExtension(
// Pull in sources from the other project. // Pull in sources from the other project.
extendSourceSet(otherProject, main) extendSourceSet(otherProject, main)
extendSourceSet(otherProject, client) extendSourceSet(otherProject, client)
for (sourceSet in listOf("datagen", "testMod", "testFixtures")) { for (sourceSet in listOf(MinecraftConfigurations.DATAGEN, MinecraftConfigurations.EXAMPLES, MinecraftConfigurations.TEST_MOD, "testFixtures")) {
otherJava.sourceSets.findByName(sourceSet)?.let { extendSourceSet(otherProject, it) } otherJava.sourceSets.findByName(sourceSet)?.let { extendSourceSet(otherProject, it) }
} }

View File

@ -24,7 +24,6 @@ class MinecraftConfigurations private constructor(private val project: Project)
private val java = project.extensions.getByType(JavaPluginExtension::class.java) private val java = project.extensions.getByType(JavaPluginExtension::class.java)
private val sourceSets = java.sourceSets private val sourceSets = java.sourceSets
private val configurations = project.configurations private val configurations = project.configurations
private val objects = project.objects
private val main = sourceSets[SourceSet.MAIN_SOURCE_SET_NAME] private val main = sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
private val test = sourceSets[SourceSet.TEST_SOURCE_SET_NAME] private val test = sourceSets[SourceSet.TEST_SOURCE_SET_NAME]
@ -37,13 +36,7 @@ class MinecraftConfigurations private constructor(private val project: Project)
val client = sourceSets.maybeCreate("client") val client = sourceSets.maybeCreate("client")
// Ensure the client classpaths behave the same as the main ones. // Ensure the client classpaths behave the same as the main ones.
configurations.named(client.compileClasspathConfigurationName) { consistentWithMain(client)
shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
}
configurations.named(client.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
}
// Set up an API configuration for clients (to ensure it's consistent with the main source set). // Set up an API configuration for clients (to ensure it's consistent with the main source set).
val clientApi = configurations.maybeCreate(client.apiConfigurationName).apply { val clientApi = configurations.maybeCreate(client.apiConfigurationName).apply {
@ -85,6 +78,16 @@ class MinecraftConfigurations private constructor(private val project: Project)
setupBasic() setupBasic()
} }
private fun consistentWithMain(sourceSet: SourceSet) {
configurations.named(sourceSet.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
}
configurations.named(sourceSet.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
}
}
private fun setupBasic() { private fun setupBasic() {
val client = sourceSets["client"] val client = sourceSets["client"]
@ -102,7 +105,24 @@ class MinecraftConfigurations private constructor(private val project: Project)
project.tasks.named("check") { dependsOn(checkDependencyConsistency) } project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
} }
/**
* Create a new configuration that pulls in the main and client classes from the mod.
*/
private fun createDerivedConfiguration(name: String) {
val client = sourceSets["client"]
val sourceSet = sourceSets.create(name)
sourceSet.compileClasspath += main.compileClasspath + client.compileClasspath
sourceSet.runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
consistentWithMain(sourceSet)
project.dependencies.add(sourceSet.implementationConfigurationName, main.output)
project.dependencies.add(sourceSet.implementationConfigurationName, client.output)
}
companion object { companion object {
const val DATAGEN = "datagen"
const val EXAMPLES = "examples"
const val TEST_MOD = "testMod"
fun setupBasic(project: Project) { fun setupBasic(project: Project) {
MinecraftConfigurations(project).setupBasic() MinecraftConfigurations(project).setupBasic()
} }
@ -110,6 +130,10 @@ class MinecraftConfigurations private constructor(private val project: Project)
fun setup(project: Project) { fun setup(project: Project) {
MinecraftConfigurations(project).setup() MinecraftConfigurations(project).setup()
} }
fun createDerivedConfiguration(project: Project, name: String) {
MinecraftConfigurations(project).createDerivedConfiguration(name)
}
} }
} }

View File

@ -4,6 +4,7 @@
package cc.tweaked.gradle package cc.tweaked.gradle
import net.neoforged.moddevgradle.internal.RunGameTask
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.FileSystemOperations
import org.gradle.api.invocation.Gradle import org.gradle.api.invocation.Gradle
@ -65,6 +66,22 @@ abstract class ClientJavaExec : JavaExec() {
setTestProperties() setTestProperties()
} }
fun copyFromForge(path: String) = copyFromForge(project.tasks.getByName(path, RunGameTask::class))
/**
* Set this task to run a given [RunGameTask].
*/
fun copyFromForge(task: RunGameTask) {
copyFrom(task)
// Eagerly evaluate the behaviour of RunGameTask.exec
environment.putAll(task.environmentProperty.get())
classpath(task.classpathProvider)
workingDir = task.gameDirectory.get().asFile
setTestProperties() // setRunConfig may clobber some properties, ensure everything is set.
}
/** /**
* Copy configuration from a task with the given name. * Copy configuration from a task with the given name.
*/ */

View File

@ -133,7 +133,7 @@ SPDX-License-Identifier: MPL-2.0
</module> </module>
<module name="MethodTypeParameterName" /> <module name="MethodTypeParameterName" />
<module name="PackageName"> <module name="PackageName">
<property name="format" value="^(dan200\.computercraft|cc\.tweaked)(\.[a-z][a-z0-9]*)*" /> <property name="format" value="^(dan200\.computercraft|cc\.tweaked|com\.example\.examplemod)(\.[a-z][a-z0-9]*)*" />
</module> </module>
<module name="ParameterName" /> <module name="ParameterName" />
<module name="StaticVariableName"> <module name="StaticVariableName">

View File

@ -68,7 +68,7 @@ ideaExt = "1.1.7"
illuaminate = "0.1.0-74-gf1551d5" illuaminate = "0.1.0-74-gf1551d5"
lwjgl = "3.3.3" lwjgl = "3.3.3"
minotaur = "2.8.7" minotaur = "2.8.7"
neoGradle = "7.0.170" modDevGradle = "2.0.74"
nullAway = "0.10.25" nullAway = "0.10.25"
shadow = "8.3.1" shadow = "8.3.1"
spotless = "6.23.3" spotless = "6.23.3"
@ -154,8 +154,8 @@ fabric-loom = { module = "net.fabricmc:fabric-loom", version.ref = "fabric-loom"
ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "ideaExt" } ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "ideaExt" }
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" } minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
neoGradle-userdev = { module = "net.neoforged.gradle:userdev", version.ref = "neoGradle" }
nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" } nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" }
modDevGradle = { module = "net.neoforged:moddev-gradle", version.ref = "modDevGradle" }
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" } teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" }
teavm-core = { module = "org.teavm:teavm-core", version.ref = "teavm" } teavm-core = { module = "org.teavm:teavm-core", version.ref = "teavm" }

View File

@ -18,13 +18,28 @@ dependencies {
api(project(":core-api")) api(project(":core-api"))
} }
val javadocOverview by tasks.registering(Copy::class) {
from("src/overview.html")
into(layout.buildDirectory.dir(name))
expand(
mapOf(
"mcVersion" to mcVersion,
"modVersion" to version,
),
)
}
tasks.javadoc { tasks.javadoc {
title = "CC: Tweaked $version Minecraft $mcVersion" title = "CC: Tweaked $version for Minecraft $mcVersion"
include("dan200/computercraft/api/**/*.java") include("dan200/computercraft/api/**/*.java")
options { options {
(this as StandardJavadocDocletOptions) (this as StandardJavadocDocletOptions)
inputs.files(javadocOverview)
overview(javadocOverview.get().destinationDir.resolve("overview.html").absolutePath)
groups = mapOf( groups = mapOf(
"Common" to listOf( "Common" to listOf(
"dan200.computercraft.api", "dan200.computercraft.api",
@ -47,6 +62,12 @@ tasks.javadoc {
<link href=" https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css " rel="stylesheet"> <link href=" https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css " rel="stylesheet">
""".trimIndent(), """.trimIndent(),
) )
val snippetSources = listOf(":common", ":fabric", ":forge").flatMap {
project(it).sourceSets["examples"].allSource.sourceDirectories
}
inputs.files(snippetSources)
addPathOption("-snippet-path").value = snippetSources
} }
// Include the core-api in our javadoc export. This is wrong, but it means we can export a single javadoc dump. // Include the core-api in our javadoc export. This is wrong, but it means we can export a single javadoc dump.

View File

@ -21,7 +21,14 @@ import java.util.stream.Stream;
* <p> * <p>
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a * Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one * modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
* on Forge * on Forge.
*
* <h2>Example</h2>
* <h3>Fabric</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h3>Forge</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
* *
* @param <T> The type of turtle upgrade this modeller applies to. * @param <T> The type of turtle upgrade this modeller applies to.
* @see RegisterTurtleUpgradeModeller For multi-loader registration support. * @see RegisterTurtleUpgradeModeller For multi-loader registration support.

View File

@ -171,16 +171,9 @@ public final class ComputerCraftAPI {
* using {@link ILuaAPI#getModuleName()} to expose this library as a module instead of as a global. * using {@link ILuaAPI#getModuleName()} to expose this library as a module instead of as a global.
* <p> * <p>
* This may be used with {@link IComputerSystem#getComponent(ComputerComponent)} to only attach APIs to specific * This may be used with {@link IComputerSystem#getComponent(ComputerComponent)} to only attach APIs to specific
* computers. For example, one can add an additional API just to turtles with the following code: * computers. For example, one can add a new API just to turtles with the following code:
* *
* {@snippet lang="java": * {@snippet class=com.example.examplemod.ExampleAPI region=register}
* ComputerCraftAPI.registerAPIFactory(computer -> {
* // Read the turtle component.
* var turtle = computer.getComponent(ComputerComponents.TURTLE);
* // If present then add our API.
* return turtle == null ? null : new MyCustomTurtleApi(turtle);
* });
* }
* *
* @param factory The factory for your API subclass. * @param factory The factory for your API subclass.
* @see ILuaAPIFactory * @see ILuaAPIFactory

View File

@ -20,32 +20,10 @@ import javax.annotation.Nullable;
* A peripheral which can be equipped to the back side of a pocket computer. * A peripheral which can be equipped to the back side of a pocket computer.
* <p> * <p>
* Pocket upgrades are defined in two stages. First, one creates a {@link IPocketUpgrade} subclass and corresponding * Pocket upgrades are defined in two stages. First, one creates a {@link IPocketUpgrade} subclass and corresponding
* {@link UpgradeType} instance, which are then registered in a registry. * {@link UpgradeType} instance, which are then registered in a Minecraft registry.
* <p> * <p>
* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and * 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 * the upgrade registered internally.
* <a href="../upgrades/UpgradeType.html#datagen">data generators</a>.
*
* <h2>Example</h2>
* {@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");
*
* // Register a new upgrade upgrade type called "my_upgrade".
* public static final RegistryObject<UpgradeType<MyUpgrade>> MY_UPGRADE =
* POCKET_UPGRADES.register("my_upgrade", () -> UpgradeType.simple(new MyUpgrade()));
*
* // Then in your constructor
* POCKET_UPGRADES.register(bus);
* }
* <p>
* We can then define a new upgrade using JSON by placing the following in
* {@code data/<my_mod>/computercraft/pocket_upgrade/<my_upgrade_id>.json}.
* {@snippet lang="json" :
* {
* "type": "my_mod:my_upgrade"
* }
* }
*/ */
public interface IPocketUpgrade extends UpgradeBase { public interface IPocketUpgrade extends UpgradeBase {
ResourceKey<Registry<IPocketUpgrade>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_upgrade")); ResourceKey<Registry<IPocketUpgrade>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_upgrade"));

View File

@ -11,47 +11,79 @@ import dan200.computercraft.api.upgrades.UpgradeType;
import dan200.computercraft.impl.ComputerCraftAPIService; import dan200.computercraft.impl.ComputerCraftAPIService;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.RegistrySetBuilder.PatchedRegistries;
import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.Function;
/** /**
* The primary interface for defining an update for Turtles. A turtle update can either be a new tool, or a new * The primary interface for defining an update for Turtles. A turtle update can either be a new tool, or a new
* peripheral. * peripheral.
* <p> * <p>
* Turtle upgrades are defined in two stages. First, one creates a {@link ITurtleUpgrade} subclass and corresponding * Turtle upgrades are defined in two stages. First, one creates a {@link ITurtleUpgrade} subclass and corresponding
* {@link UpgradeType} instance, which are then registered in a registry. * {@link UpgradeType} instance, which are then registered in a Minecraft registry.
* <p> * <p>
* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and * 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 * the upgrade automatically registered.
* <a href="../upgrades/UpgradeType.html#datagen">data generators</a>.
* *
* <h2>Example</h2> * <h2>Example</h2>
* {@snippet lang="java" : * <h3>Registering the upgrade type</h3>
* // We use Forge's DeferredRegister to register our upgrade type. Fabric mods may register their type directly. * First, let's create a new class that implements {@link ITurtleUpgrade}. It is recommended to subclass
* static final DeferredRegister<UpgradeType<? extends ITurtleUpgrade>> TURTLE_UPGRADES = DeferredRegister.create(ITurtleUpgrade.typeRegistry(), "my_mod"); * {@link AbstractTurtleUpgrade}, as that provides a default implementation of most methods.
* *
* // Register a new upgrade type called "my_upgrade". * {@snippet class=com.example.examplemod.ExampleTurtleUpgrade region=body}
* public static final RegistryObject<UpgradeType<MyUpgrade>> MY_UPGRADE =
* TURTLE_UPGRADES.register("my_upgrade", () -> UpgradeType.simple(MyUpgrade::new));
* *
* // Then in your constructor * Now we must construct a new upgrade type. In most cases, you can use one of the helper methods (e.g.
* TURTLE_UPGRADES.register(bus); * {@link UpgradeType#simpleWithCustomItem(Function)}), rather than defining your own implementation.
* } *
* {@snippet class=com.example.examplemod.ExampleMod region=turtle_upgrades}
*
* We now must register this upgrade type. This is done the same way as you'd register blocks, items, or other
* Minecraft objects. The approach to do this will depend on mod-loader.
*
* <h4>Fabric</h4>
* {@snippet class=com.example.examplemod.FabricExampleMod region=turtle_upgrades}
*
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.ForgeExampleMod region=turtle_upgrades}
*
* <h3>Rendering the upgrade</h3>
* Next, we need to register a model for our upgrade. This is done by registering a
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for your upgrade type.
*
* <h4>Fabric</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers}
*
* <h3 id="datagen">Registering the upgrade itself</h3>
* Upgrades themselves are loaded from datapacks when a level is loaded. In order to register our new upgrade, we must
* create a new JSON file at {@code data/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
*
* {@snippet file=data/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json}
*
* The {@code "type"} field points to the ID of the upgrade type we've just registered, while the other fields are read
* by the type itself. As our upgrade was defined with {@link UpgradeType#simpleWithCustomItem(Function)}, the
* {@code "item"} field will construct our upgrade with {@link Items#COMPASS}.
* <p> * <p>
* We can then define a new upgrade using JSON by placing the following in * Rather than manually creating the file, it is recommended to use data-generators to generate this file. First, we
* {@code data/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}. * register our new upgrades into a {@linkplain PatchedRegistries patched registry}.
* <p> *
* {@snippet lang="json" : * {@snippet class=com.example.examplemod.data.TurtleUpgradeProvider region=body}
* { *
* "type": "my_mod:my_upgrade" * Next, we must write these upgrades to disk. Vanilla does not have complete support for this yet, so this must be done
* } * with mod-loader-specific APIs.
* } *
* <p> * <h4>Fabric</h4>
* Finally, we need to register a model for our upgrade, see * {@snippet class=com.example.examplemod.FabricExampleModDataGenerator region=turtle_upgrades}
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information. *
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.ForgeExampleModDataGenerator region=turtle_upgrades}
*/ */
public interface ITurtleUpgrade extends UpgradeBase { public interface ITurtleUpgrade extends UpgradeBase {
/** /**

View File

@ -25,19 +25,11 @@ import java.util.Optional;
/** /**
* A builder for custom turtle tool upgrades. * A builder for custom turtle tool upgrades.
* <p> * <p>
* This can be used from your <a href="../upgrades/UpgradeType.html#datagen">data generator</a> code in order to * This can be used from your <a href="./ITurtleUpgrade.html#datagen">data generator</a> code in order to
* register turtle tools for your mod's tools. * register turtle tools for your mod's tools.
* *
* <h2>Example:</h2> * <h2>Example</h2>
* {@snippet lang = "java": * {@snippet class=com.example.examplemod.data.TurtleToolProvider region=body}
* 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 { public final class TurtleToolBuilder {
private final ResourceKey<ITurtleUpgrade> id; private final ResourceKey<ITurtleUpgrade> id;

View File

@ -7,9 +7,7 @@ package dan200.computercraft.api.upgrades;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.impl.upgrades.UpgradeTypeImpl;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.data.registries.RegistryPatchGenerator;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import net.minecraft.world.level.storage.loot.functions.LootItemFunction;
@ -23,13 +21,10 @@ import java.util.function.Function;
* follow a similar design to other dynamic content, such as {@linkplain Recipe recipes} or {@link LootItemFunction * follow a similar design to other dynamic content, such as {@linkplain Recipe recipes} or {@link LootItemFunction
* loot functions}. * loot functions}.
* <p> * <p>
* First, one adds a new class implementing {@link ITurtleUpgrade} or {@link IPocketUpgrade}). This is responsible for * While the {@link ITurtleUpgrade}/{@link IPocketUpgrade} class should contain the core logic of the upgrade, they are
* handling all the logic of your upgrade. * not registered directly. Instead, each upgrade class has a corresponding {@link UpgradeType}, which is responsible
* <p> * for loading the upgrade from a datapack. The upgrade type should then be registered in its appropriate registry
* However, the upgrades are not registered directly. Instead, each upgrade class should have a corresponding * ({@link ITurtleUpgrade#typeRegistry()}, {@link IPocketUpgrade#typeRegistry()}).
* {@link UpgradeType}, which is responsible for loading the upgrade from a datapack. The upgrade type should then be
* registered in its appropriate registry ({@link ITurtleUpgrade#typeRegistry()},
* {@link IPocketUpgrade#typeRegistry()}).
* <p> * <p>
* In order to register the actual upgrade, a JSON file referencing your upgrade type should be added to a datapack. It * 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. * is recommended to do this via the data generators.
@ -38,29 +33,7 @@ import java.util.function.Function;
* As turtle and pocket upgrades are just loaded using vanilla's dynamic loaders, one may use the same data generation * 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. * tools as you would for any other dynamic registry.
* <p> * <p>
* This can typically be done by extending vanilla's built-in registries using {@link RegistryPatchGenerator}, and then * See <a href="../turtle/ITurtleUpgrade.html#datagen">the turtle upgrade docs</a> for a concrete example.
* 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. * @param <T> The upgrade subclass that this upgrade type represents.
* @see ITurtleUpgrade * @see ITurtleUpgrade

View File

@ -2,12 +2,9 @@
// //
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.upgrades; package dan200.computercraft.api.upgrades;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.api.upgrades.UpgradeType;
import org.jetbrains.annotations.ApiStatus;
/** /**
* Simple implementation of {@link UpgradeType}. * Simple implementation of {@link UpgradeType}.
@ -15,6 +12,5 @@ import org.jetbrains.annotations.ApiStatus;
* @param codec The codec to read/write upgrades with. * @param codec The codec to read/write upgrades with.
* @param <T> The upgrade subclass that this upgrade type represents. * @param <T> The upgrade subclass that this upgrade type represents.
*/ */
@ApiStatus.Internal record UpgradeTypeImpl<T extends UpgradeBase>(MapCodec<T> codec) implements UpgradeType<T> {
public record UpgradeTypeImpl<T extends UpgradeBase>(MapCodec<T> codec) implements UpgradeType<T> {
} }

View File

@ -0,0 +1,68 @@
<!--
SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0
-->
<!DOCTYPE HTML>
<html lang="en">
<body>
<p>
This is the documentation for CC: Tweaked $modVersion for Minecraft $mcVersion. Documentation for other versions of
Minecraft are available on the CC: Tweaked website:
<ul>
<li><a href="/mc-1.20.x/javadoc/">Minecraft 1.20.1</a>
<li><a href="/mc-1.21.x/javadoc/">Minecraft 1.21.1</a>
</ul>
<h1>Quick links</h1>
<p>
You probably want to start in the following places:
<ul>
<li>{@linkplain dan200.computercraft.api.peripheral Registering new peripherals}</li>
<li>
{@link dan200.computercraft.api.lua.LuaFunction} and {@link dan200.computercraft.api.lua.IArguments} for
adding methods to your peripheral or Lua objects.
</li>
<li>{@linkplain dan200.computercraft.api.turtle.ITurtleUpgrade Turtle upgrades}</li>
<li>{@linkplain dan200.computercraft.api.pocket.IPocketUpgrade Pocket upgrades}</li>
</ul>
<h1>Using</h1>
<p>
CC: Tweaked is hosted on my maven repo, and so is relatively simple to depend on. You may wish to add a soft (or
hard) dependency in your <code>mods.toml</code> file, with the appropriate version bounds, to ensure that API
functionality you depend on is present.
<pre class="language language-groovy"><code>repositories {
maven {
url "https://maven.squiddev.cc"
content { includeGroup("cc.tweaked") }
}
}
dependencies {
// Vanilla (i.e. for multi-loader systems)
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api:$modVersion")
// Forge Gradle
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-core-api:$modVersion")
compileOnly(fg.deobf("cc.tweaked:cc-tweaked-$mcVersion-forge-api:$modVersion"))
runtimeOnly(fg.deobf("cc.tweaked:cc-tweaked-$mcVersion-forge:$modVersion"))
// Fabric Loom
modCompileOnly("cc.tweaked:cc-tweaked-$mcVersion-fabric-api:$modVersion")
modRuntimeOnly("cc.tweaked:cc-tweaked-$mcVersion-fabric:$modVersion")
}
</code></pre>
<p>
You should also be careful to only use classes within the <code>dan200.computercraft.api</code> package. Non-API
classes are subject to change at any point. If you depend on functionality outside the API (or need to mixin to
CC:T), please <a href="https://github.com/cc-tweaked/CC-Tweaked/discussions/new/choose">start a discussion</a> to
let me know!
</body>
</html>

View File

@ -11,12 +11,6 @@ plugins {
id("cc-tweaked.publishing") id("cc-tweaked.publishing")
} }
sourceSets {
main {
resources.srcDir("src/generated/resources")
}
}
minecraft { minecraft {
accessWideners( accessWideners(
"src/main/resources/computercraft.accesswidener", "src/main/resources/computercraft.accesswidener",
@ -115,20 +109,28 @@ val lintLua by tasks.registering(IlluaminateExec::class) {
doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") } doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
} }
val runData by tasks.registering(MergeTrees::class) { fun MergeTrees.configureForDatagen(source: SourceSet, outputFolder: String) {
output = layout.projectDirectory.dir("src/generated/resources") output = layout.projectDirectory.dir(outputFolder)
for (loader in listOf("forge", "fabric")) { for (loader in listOf("forge", "fabric")) {
mustRunAfter(":$loader:runData") mustRunAfter(":$loader:$name")
source { source {
input { input {
from(project(":$loader").layout.buildDirectory.dir("generatedResources")) from(project(":$loader").layout.buildDirectory.dir(source.getTaskName("generateResources", null)))
exclude(".cache") exclude(".cache")
} }
output = project(":$loader").layout.projectDirectory.dir("src/generated/resources") output = project(":$loader").layout.projectDirectory.dir(outputFolder)
} }
} }
} }
val runData by tasks.registering(MergeTrees::class) {
configureForDatagen(sourceSets.main.get(), "src/generated/resources")
}
val runExampleData by tasks.registering(MergeTrees::class) {
configureForDatagen(sourceSets.examples.get(), "src/examples/generatedResources")
}
tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false } tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false }

View File

@ -0,0 +1,4 @@
{
"type": "examplemod:example_turtle_upgrade",
"item": "minecraft:compass"
}

View File

@ -0,0 +1,75 @@
package com.example.examplemod;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.turtle.ITurtleAccess;
import org.jetbrains.annotations.Nullable;
/**
* An example API that will be available on every turtle. This demonstrates both registering an API, and how to write
* Lua-facing functions.
* <p>
* This API is not available as a global (as {@link #getNames() returns nothing}), but is instead accessible via
* {@code require} (see {@link #getModuleName()}).
*
* <h2>Example</h2>
* <pre class="language language-lua">{@code
* local my_api = require("example.my_api")
* print("Turtle is facing " .. my_api.getDirection())
* }</pre>
*/
public class ExampleAPI implements ILuaAPI {
private final ITurtleAccess turtle;
public ExampleAPI(ITurtleAccess turtle) {
this.turtle = turtle;
}
public static void register() {
// @start region=register
ComputerCraftAPI.registerAPIFactory(computer -> {
// Read the turtle component.
var turtle = computer.getComponent(ComputerComponents.TURTLE);
// If present then add our API.
return turtle == null ? null : new ExampleAPI(turtle);
});
// @end region=register
}
@Override
public String[] getNames() {
return new String[0];
}
@Override
public @Nullable String getModuleName() {
return "example.my_api";
}
/**
* A Lua-facing function function that returns the direction the turtle is facing.
*
* @return The turtle's direction.
*/
@LuaFunction
public final String getDirection() {
return turtle.getDirection().getName();
}
/**
* A Lua-facing function using {@link Coerced}. Unlike a {@link LuaFunction} taking a raw {@link String}, this will
* accept any value, and convert it to a string.
*
* @param myString The value to write.
*/
// @start region=coerced
@LuaFunction
public final void writeString(Coerced<String> myString) {
String contents = myString.value();
System.out.println("Got " + contents);
}
// @end region=coerced
}

View File

@ -0,0 +1,39 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleUpgradeProvider;
import com.example.examplemod.peripheral.FurnacePeripheral;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.upgrades.UpgradeType;
/**
* Our example mod, containing the various things we register.
* <p>
* This isn't an especially good template to follow! It's convenient for our example mod (as we need to be multi-loader
* compatible), but there's a good chance there's a better pattern to follow. For example, on Forge you'd use
* {@code DeferredRegister} to register things), and multi-loader mods probably have their own abstractions.
* <p>
* See {@code FabricExampleMod} and {@code ForgeExampleMod} for the actual mod entrypoints.
*/
public final class ExampleMod {
public static final String MOD_ID = "examplemod";
/**
* The upgrade type for our example turtle upgrade. See the documentation for {@link UpgradeType} or
* {@code FabricExampleMod}/{@code ForgeExampleMod} for how this is registered.
* <p>
* This only defines the upgrade type. See {@link TurtleUpgradeProvider} for defining the actual upgrade.
*/
// @start region=turtle_upgrades
public static final UpgradeType<ExampleTurtleUpgrade> EXAMPLE_TURTLE_UPGRADE = UpgradeType.simpleWithCustomItem(
ExampleTurtleUpgrade::new
);
// @end region=turtle_upgrades
public static void registerComputerCraft() {
// @start region=generic_source
ComputerCraftAPI.registerGenericSource(new FurnacePeripheral());
// @end region=generic_source
ExampleAPI.register();
}
}

View File

@ -0,0 +1,22 @@
package com.example.examplemod;
import dan200.computercraft.api.turtle.AbstractTurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeType;
import dan200.computercraft.api.upgrades.UpgradeType;
import net.minecraft.world.item.ItemStack;
/**
* An example turtle upgrade.
*/
// @start region=body
public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade {
public ExampleTurtleUpgrade(ItemStack stack) {
super(TurtleUpgradeType.PERIPHERAL, "example", stack);
}
@Override
public UpgradeType<ExampleTurtleUpgrade> getType() {
return ExampleMod.EXAMPLE_TURTLE_UPGRADE;
}
}
// @end region=body

View File

@ -0,0 +1,23 @@
package com.example.examplemod.data;
import com.example.examplemod.ExampleMod;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleToolBuilder;
import net.minecraft.data.worldgen.BootstrapContext;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;
/**
* Extends the bootstrap registries with a new turtle tool.
* <p>
* See {@link TurtleUpgradeProvider} and {@link ITurtleUpgrade} for how this would be used in datagen.
*/
public class TurtleToolProvider {
// @start region=body
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
TurtleToolBuilder
.tool(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "wooden_pickaxe"), Items.WOODEN_PICKAXE)
.register(upgrades);
}
// @end region=body
}

View File

@ -0,0 +1,37 @@
package com.example.examplemod.data;
import com.example.examplemod.ExampleMod;
import com.example.examplemod.ExampleTurtleUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import net.minecraft.Util;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder;
import net.minecraft.data.registries.RegistryPatchGenerator;
import net.minecraft.data.worldgen.BootstrapContext;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.concurrent.CompletableFuture;
/**
* Extends the bootstrap registries with our {@linkplain ExampleTurtleUpgrade example turtle upgrade}.
*/
// @start region=body
public class TurtleUpgradeProvider {
// Register our turtle upgrades.
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(
ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade")),
new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS))
);
}
// Set up the dynamic registries to contain our turtle upgrades.
public static CompletableFuture<RegistrySetBuilder.PatchedRegistries> makeUpgradeRegistry(CompletableFuture<HolderLookup.Provider> registries) {
return RegistryPatchGenerator.createLookup(registries, Util.make(new RegistrySetBuilder(), builder -> {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
}));
}
}
// @end region=body

View File

@ -0,0 +1,12 @@
@ApiStatus.Internal
@DefaultQualifier(value = NonNull.class, locations = {
TypeUseLocation.RETURN,
TypeUseLocation.PARAMETER,
TypeUseLocation.FIELD,
})
package com.example.examplemod;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.checkerframework.framework.qual.TypeUseLocation;
import org.jetbrains.annotations.ApiStatus;

View File

@ -0,0 +1,39 @@
package com.example.examplemod.peripheral;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
import org.jetbrains.annotations.Nullable;
/**
* A peripheral that adds a {@code getFuel()} method to brewing stands. This demonstrates the usage of
* {@link IPeripheral}.
*
* @see dan200.computercraft.api.peripheral
* @see FurnacePeripheral Using {@code GenericPeripheral}.
*/
// @start region=body
public class BrewingStandPeripheral implements IPeripheral {
private final BrewingStandBlockEntity brewingStand;
public BrewingStandPeripheral(BrewingStandBlockEntity brewingStand) {
this.brewingStand = brewingStand;
}
@Override
public String getType() {
return "brewing_stand";
}
@LuaFunction
public final int getFuel() {
// Don't do it this way! Use an access widener/transformer to access the "fuel" field instead.
return brewingStand.saveWithoutMetadata(brewingStand.getLevel().registryAccess()).getInt("Fuel");
}
@Override
public boolean equals(@Nullable IPeripheral other) {
return other instanceof BrewingStandPeripheral o && brewingStand == o.brewingStand;
}
}
// @end region=body

View File

@ -0,0 +1,44 @@
package com.example.examplemod.peripheral;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.AttachedComputerSet;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import org.jetbrains.annotations.Nullable;
/**
* A peripheral that tracks what computers it is attached to.
*
* @see AttachedComputerSet
*/
// @start region=body
public class ComputerTrackingPeripheral implements IPeripheral {
private final AttachedComputerSet computers = new AttachedComputerSet();
@Override
public void attach(IComputerAccess computer) {
computers.add(computer);
}
@Override
public void detach(IComputerAccess computer) {
computers.remove(computer);
}
@LuaFunction
public final void sayHello() {
// Queue a "hello" event on each computer.
computers.forEach(x -> x.queueEvent("hello", x.getAttachmentName()));
}
@Override
public String getType() {
return "my_peripheral";
}
@Override
public boolean equals(@Nullable IPeripheral other) {
return this == other;
}
}
// @end region=body

View File

@ -0,0 +1,29 @@
package com.example.examplemod.peripheral;
import com.example.examplemod.ExampleMod;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
/**
* A peripheral that adds a {@code getBurnTime} method to furnaces. This is used to demonstrate the usage of
* {@link GenericPeripheral}.
*
* @see dan200.computercraft.api.peripheral
* @see BrewingStandPeripheral Using {@code IPeripheral}.
*/
// @start region=body
public class FurnacePeripheral implements GenericPeripheral {
@Override
public String id() {
return ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "furnace").toString();
}
@LuaFunction(mainThread = true)
public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
// Don't do it this way! Use an access widener/transformer to access the "litTime" field instead.
return furnace.saveWithoutMetadata(furnace.getLevel().registryAccess()).getInt("BurnTime");
}
}
// @end region=body

View File

@ -747,6 +747,7 @@ public class TurtleAPI implements ILuaAPI {
* @throws LuaException If the slot is out of range. * @throws LuaException If the slot is out of range.
* @cc.treturn nil|table Information about the given slot, or {@code nil} if it is empty. * @cc.treturn nil|table Information about the given slot, or {@code nil} if it is empty.
* @cc.since 1.64 * @cc.since 1.64
* @cc.changed 1.90.0 Added detailed parameter.
* @cc.usage Print the current slot, assuming it contains 13 dirt. * @cc.usage Print the current slot, assuming it contains 13 dirt.
* *
* <pre>{@code * <pre>{@code

View File

@ -8,10 +8,6 @@ plugins {
id("cc-tweaked") id("cc-tweaked")
} }
java {
withJavadocJar()
}
// Due to the slightly circular nature of our API, add the main API jars to the javadoc classpath. // Due to the slightly circular nature of our API, add the main API jars to the javadoc classpath.
val docApi by configurations.registering { val docApi by configurations.registering {
isTransitive = false isTransitive = false

View File

@ -10,13 +10,8 @@ package dan200.computercraft.api.lua;
* This is designed to be used with {@link LuaFunction} annotated functions, to mark an argument as being coerced to * This is designed to be used with {@link LuaFunction} annotated functions, to mark an argument as being coerced to
* the given type, rather than requiring an exact type. * the given type, rather than requiring an exact type.
* *
* <h2>Example:</h2> * <h2>Example</h2>
* {@snippet lang="java" : * {@snippet class=com.example.examplemod.ExampleAPI region=coerced}
* @LuaFunction
* public final void doSomething(Coerced<String> myString) {
* var value = myString.value();
* }
* }
* *
* @param value The argument value. * @param value The argument value.
* @param <T> The type of the underlying value. * @param <T> The type of the underlying value.

View File

@ -18,22 +18,11 @@ import dan200.computercraft.api.peripheral.IPeripheral;
* by capabilities/the block lookup API take priority. Block entities which use this system are given a peripheral name * by capabilities/the block lookup API take priority. Block entities which use this system are given a peripheral name
* determined by their id, rather than any peripheral provider, though additional types may be provided by overriding * determined by their id, rather than any peripheral provider, though additional types may be provided by overriding
* {@link GenericPeripheral#getType()}. * {@link GenericPeripheral#getType()}.
* <p>
* For example, the main CC: Tweaked mod defines a generic source for inventories, which works on {@code IItemHandler}s:
* *
* {@snippet lang="java" : * <h2>Example</h2>
* public class InventoryMethods implements GenericSource { * {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body}
* @LuaFunction(mainThread = true)
* public int size(IItemHandler inventory) {
* return inventory.getSlots();
* }
*
* // ...
* }
* }
* <p> * <p>
* New capabilities or block lookups (those not built into Forge/Fabric) must be explicitly registered using the * New capabilities (those not built into Forge) must be explicitly registered using the loader-specific API.
* loader-specific API.
* *
* @see dan200.computercraft.api.ComputerCraftAPI#registerGenericSource(GenericSource) * @see dan200.computercraft.api.ComputerCraftAPI#registerGenericSource(GenericSource)
*/ */

View File

@ -4,17 +4,6 @@
/** /**
* ComputerCraft's public API. * ComputerCraft's public API.
* <p>
* You probably want to start in the following places:
* <ul>
* <li>{@link dan200.computercraft.api.peripheral} for registering new peripherals.</li>
* <li>
* {@link dan200.computercraft.api.lua.LuaFunction} and {@link dan200.computercraft.api.lua.IArguments} for
* adding methods to your peripheral or Lua objects.
* </li>
* <li>{@link dan200.computercraft.api.turtle.ITurtleUpgrade} for turtle upgrades.</li>
* <li>{@link dan200.computercraft.api.pocket.IPocketUpgrade} for pocket upgrades.</li>
* </ul>
*/ */
@DefaultQualifier(value = NonNull.class, locations = { @DefaultQualifier(value = NonNull.class, locations = {
TypeUseLocation.RETURN, TypeUseLocation.RETURN,

View File

@ -27,21 +27,7 @@ import java.util.function.Consumer;
* *
* <h2>Example</h2> * <h2>Example</h2>
* *
* {@snippet lang="java" : * {@snippet class=com.example.examplemod.peripheral.ComputerTrackingPeripheral region=body}
* public class MyPeripheral implements IPeripheral {
* private final AttachedComputerSet computers = new ComputerCollection();
*
* @Override
* public void attach(IComputerAccess computer) {
* computers.add(computer);
* }
*
* @Override
* public void detach(IComputerAccess computer) {
* computers.remove(computer);
* }
* }
* }
* *
* @see IComputerAccess * @see IComputerAccess
* @see IPeripheral#attach(IComputerAccess) * @see IPeripheral#attach(IComputerAccess)

View File

@ -47,36 +47,12 @@
* Then, we can start adding methods to your block entity. Each method should take its target type as the first * Then, we can start adding methods to your block entity. Each method should take its target type as the first
* argument, which in this case is a {@code AbstractFurnaceBlockEntity}. We then annotate this method with * argument, which in this case is a {@code AbstractFurnaceBlockEntity}. We then annotate this method with
* {@link dan200.computercraft.api.lua.LuaFunction} to expose it to computers. * {@link dan200.computercraft.api.lua.LuaFunction} to expose it to computers.
* <p>
* {@snippet lang="java" :
* import dan200.computercraft.api.lua.LuaFunction;
* import dan200.computercraft.api.peripheral.GenericPeripheral;
* import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
* *
* public final class FurnacePeripheral implements GenericPeripheral { * {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body}
* @Override
* public String id() {
* return "mymod:furnace";
* }
* *
* @LuaFunction(mainThread = true)
* public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
* return furnace.litTime;
* }
* }
* }
* <p>
* Finally, we need to register our peripheral, so that ComputerCraft is aware of it: * Finally, we need to register our peripheral, so that ComputerCraft is aware of it:
* <p>
* {@snippet lang="java" :
* import dan200.computercraft.api.ComputerCraftAPI;
* *
* public class ComputerCraftCompat { * {@snippet class=com.example.examplemod.ExampleMod region=generic_source}
* public static void register() {
* ComputerCraftAPI.registerGenericSource(new FurnacePeripheral());
* }
* }
* }
* *
* <h3>Creating a {@code IPeripheral}</h3> * <h3>Creating a {@code IPeripheral}</h3>
* First, we'll need to create a new class that implements {@link dan200.computercraft.api.peripheral.IPeripheral}. This * First, we'll need to create a new class that implements {@link dan200.computercraft.api.peripheral.IPeripheral}. This
@ -84,72 +60,20 @@
* <p> * <p>
* We can then start adding peripheral methods to our class. Each method should be {@code final}, and annotated with * We can then start adding peripheral methods to our class. Each method should be {@code final}, and annotated with
* {@link dan200.computercraft.api.lua.LuaFunction}. * {@link dan200.computercraft.api.lua.LuaFunction}.
* <p>
* {@snippet lang="java" :
* import dan200.computercraft.api.lua.LuaFunction;
* import dan200.computercraft.api.peripheral.IPeripheral;
* import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
* import org.jetbrains.annotations.Nullable;
* *
* public class FurnacePeripheral implements IPeripheral { * {@snippet class=com.example.examplemod.peripheral.BrewingStandPeripheral region=body}
* private final AbstractFurnaceBlockEntity furnace;
* *
* public FurnacePeripheral(AbstractFurnaceBlockEntity furnace) {
* this.furnace = furnace;
* }
*
* @Override
* public String getType() {
* return "furnace";
* }
*
* @LuaFunction(mainThread = true)
* public final int getBurnTime() {
* return furnace.litTime;
* }
*
* @Override
* public boolean equals(@Nullable IPeripheral other) {
* return this == other || other instanceof FurnacePeripheral p && furnace == p.furnace;
* }
* }
* }
* <p>
* Finally, we'll need to register our peripheral. This is done with capabilities on Forge, or the block lookup API on * Finally, we'll need to register our peripheral. This is done with capabilities on Forge, or the block lookup API on
* Fabric. * Fabric.
* *
* <h4>Registering {@code IPeripheral} on Forge</h4> * <h4>Registering {@code IPeripheral} on Forge</h4>
* Registering a peripheral on Forge can be done by using the capability API, via {@code PeripheralCapability}. * Registering a peripheral on Forge can be done by using the capability API, via {@code PeripheralCapability}.
* *
* {@snippet lang="java" : * {@snippet class=com.example.examplemod.ForgeExampleMod region=peripherals}
* import dan200.computercraft.api.peripheral.PeripheralCapability;
* import net.minecraft.world.level.block.entity.BlockEntityType;
* import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
*
* public class ComputerCraftCompat {
* public static void register(RegisterCapabilitiesEvent event) {
* event.registerBlockEntity(PeripheralCapability.get(), BlockEntityType.FURNACE, (f, s) -> new FurnacePeripheral(f));
* event.registerBlockEntity(PeripheralCapability.get(), BlockEntityType.BLAST_FURNACE, (f, s) -> new FurnacePeripheral(f));
* event.registerBlockEntity(PeripheralCapability.get(), BlockEntityType.SMOKER, (f, s) -> new FurnacePeripheral(f));
* }
* }
* }
* *
* <h4>Registering {@code IPeripheral} on Fabric</h4> * <h4>Registering {@code IPeripheral} on Fabric</h4>
* Registering a peripheral on Fabric can be done using the block lookup API, via {@code PeripheralLookup}. * Registering a peripheral on Fabric can be done using the block lookup API, via {@code PeripheralLookup}.
* *
* {@snippet lang="java" : * {@snippet class=com.example.examplemod.FabricExampleMod region=peripherals}
* import dan200.computercraft.api.peripheral.PeripheralLookup;
* import dan200.computercraft.example.FurnacePeripheral;
* import net.minecraft.world.level.block.entity.BlockEntityType;
*
* public class ComputerCraftCompat {
* public static void register() {
* PeripheralLookup.get().registerForBlockEntity((f, s) -> new FurnacePeripheral(f), BlockEntityType.FURNACE);
* PeripheralLookup.get().registerForBlockEntity((f, s) -> new FurnacePeripheral(f), BlockEntityType.BLAST_FURNACE);
* PeripheralLookup.get().registerForBlockEntity((f, s) -> new FurnacePeripheral(f), BlockEntityType.SMOKER);
* }
* }
* }
*/ */
package dan200.computercraft.api.peripheral; package dan200.computercraft.api.peripheral;

View File

@ -13,7 +13,7 @@ import java.util.Set;
* This is intended for logging errors where the message content is supplied from untrusted sources. This isn't a * This is intended for logging errors where the message content is supplied from untrusted sources. This isn't a
* perfect escaping mechanism, but ensures basic "unsafe" strings (i.e. ANSI escape sequences, long lines) are escaped. * perfect escaping mechanism, but ensures basic "unsafe" strings (i.e. ANSI escape sequences, long lines) are escaped.
* *
* <h2>Example:</h2> * <h2>Example</h2>
* <pre>{@code * <pre>{@code
* LOG.error("Some error occurred: {}", new TruncatedError(error)); * LOG.error("Some error occurred: {}", new TruncatedError(error));
* }</pre> * }</pre>

View File

@ -223,16 +223,21 @@ end
--- Returns true if a path is mounted to the parent filesystem. --- Returns true if a path is mounted to the parent filesystem.
-- --
-- The root filesystem "/" is considered a mount, along with disk folders and -- The root filesystem "/" is considered a mount, along with disk folders and
-- the rom folder. Other programs (such as network shares) can exstend this to -- the rom folder.
-- make other mount types by correctly assigning their return value for getDrive.
-- --
-- @tparam string path The path to check. -- @tparam string path The path to check.
-- @treturn boolean If the path is mounted, rather than a normal file/folder. -- @treturn boolean If the path is mounted, rather than a normal file/folder.
-- @throws If the path does not exist. -- @throws If the path does not exist.
-- @see getDrive -- @see getDrive
-- @since 1.87.0 -- @since 1.87.0
function fs.isDriveRoot(sPath) function fs.isDriveRoot(path)
expect(1, sPath, "string") expect(1, path, "string")
local parent = fs.getDir(path)
-- Force the root directory to be a mount. -- Force the root directory to be a mount.
return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath)) if parent == ".." then return true end
local drive = fs.getDrive(path)
return drive ~= nil and drive ~= fs.getDrive(parent)
end end

View File

@ -147,7 +147,7 @@ handleMetatable = {
if format == "l" then if format == "l" then
if handle.readLine then res = handle.readLine() end if handle.readLine then res = handle.readLine() end
elseif format == "L" and handle.readLine then elseif format == "L" then
if handle.readLine then res = handle.readLine(true) end if handle.readLine then res = handle.readLine(true) end
elseif format == "a" then elseif format == "a" then
if handle.readAll then res = handle.readAll() or "" end if handle.readAll then res = handle.readAll() or "" end

View File

@ -35,7 +35,7 @@ local function getFilename(sUrl)
return sUrl:match("/([^/]+)$") return sUrl:match("/([^/]+)$")
end end
local function get(sUrl) local function get(url)
-- Check if the URL is valid -- Check if the URL is valid
local ok, err = http.checkURL(url) local ok, err = http.checkURL(url)
if not ok then if not ok then
@ -43,12 +43,12 @@ local function get(sUrl)
return return
end end
write("Connecting to " .. sUrl .. "... ") write("Connecting to " .. url .. "... ")
local response = http.get(sUrl) local response, err = http.get(url)
if not response then if not response then
print("Failed.") printError(err)
return nil return
end end
print("Success.") print("Success.")

View File

@ -104,7 +104,7 @@ while running do
end end
else else
printError(results[2]) printError(results[2])
require "cc.internal.exception".report(results[2], results[3], chunk_map) exception.report(results[2], results[3], chunk_map)
end end
else else
local parser = require "cc.internal.syntax" local parser = require "cc.internal.syntax"

View File

@ -83,6 +83,10 @@ describe("The fs library", function()
expect(fs.isDriveRoot("/rom/startup.lua")):eq(false) expect(fs.isDriveRoot("/rom/startup.lua")):eq(false)
expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false) expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false)
end) end)
it("returns false for missing files", function()
expect(fs.isDriveRoot("does_not_exist")):eq(false)
end)
end) end)
describe("fs.list", function() describe("fs.list", function()
@ -555,6 +559,22 @@ describe("The fs library", function()
end) end)
end) end)
describe("fs.getDrive", function()
it("returns the drive for the mount roots", function()
expect(fs.getDrive("")):eq("hdd")
expect(fs.getDrive("rom")):eq("rom")
end)
it("returns the drive for subdirectories", function()
expect(fs.getDrive("rom/startup.lua")):eq("rom")
end)
it("returns nothing for missing files", function()
-- Peculiar, but we return no values, rather than nil!
expect(table.pack(fs.getDrive("no_such_file"))):same { n = 0 }
end)
end)
describe("fs.attributes", function() describe("fs.attributes", function()
it("errors on non-existent files", function() it("errors on non-existent files", function()
expect.error(fs.attributes, "xuxu_nao_existe"):eq("/xuxu_nao_existe: No such file") expect.error(fs.attributes, "xuxu_nao_existe"):eq("/xuxu_nao_existe: No such file")

View File

@ -7,10 +7,6 @@ plugins {
id("cc-tweaked.publishing") id("cc-tweaked.publishing")
} }
java {
withJavadocJar()
}
cct.inlineProject(":common-api") cct.inlineProject(":common-api")
dependencies { dependencies {

View File

@ -106,8 +106,6 @@ dependencies {
testFixturesImplementation(testFixtures(project(":core"))) testFixturesImplementation(testFixtures(project(":core")))
} }
sourceSets.main { resources.srcDir("src/generated/resources") }
loom { loom {
accessWidenerPath.set(project(":common").file("src/main/resources/computercraft.accesswidener")) accessWidenerPath.set(project(":common").file("src/main/resources/computercraft.accesswidener"))
mixin.useLegacyMixinAp = false mixin.useLegacyMixinAp = false
@ -126,6 +124,10 @@ loom {
sourceSet(sourceSets.testMod.get()) sourceSet(sourceSets.testMod.get())
sourceSet(project(":common").sourceSets.testMod.get()) sourceSet(project(":common").sourceSets.testMod.get())
} }
register("examplemod") {
sourceSet(sourceSets.examples.get())
}
} }
runs { runs {
@ -143,19 +145,24 @@ loom {
runDir("run/server") runDir("run/server")
} }
register("data") { fun RunConfigSettings.configureForData(sourceSet: SourceSet) {
configName = "Datagen"
client() client()
runDir("run/run${name.capitalise()}")
source(sourceSets.datagen.get())
runDir("run/dataGen")
property("fabric-api.datagen") property("fabric-api.datagen")
property("fabric-api.datagen.output-dir", layout.buildDirectory.dir("generatedResources").getAbsolutePath()) property(
"fabric-api.datagen.output-dir",
layout.buildDirectory.dir(sourceSet.getTaskName("generateResources", null)).getAbsolutePath(),
)
property("fabric-api.datagen.strict-validation") property("fabric-api.datagen.strict-validation")
} }
fun configureForGameTest(config: RunConfigSettings) = config.run { register("data") {
configName = "Datagen"
configureForData(sourceSets.main.get())
source(sourceSets.datagen.get())
}
fun RunConfigSettings.configureForGameTest() {
source(sourceSets.testMod.get()) source(sourceSets.testMod.get())
val testSources = project(":common").file("src/testMod/resources/data/cctest").absolutePath val testSources = project(":common").file("src/testMod/resources/data/cctest").absolutePath
@ -170,7 +177,7 @@ loom {
val testClient by registering { val testClient by registering {
configName = "Test Client" configName = "Test Client"
client() client()
configureForGameTest(this) configureForGameTest()
runDir("run/testClient") runDir("run/testClient")
property("cctest.tags", "client,common") property("cctest.tags", "client,common")
@ -179,16 +186,27 @@ loom {
register("gametest") { register("gametest") {
configName = "Game Test" configName = "Game Test"
server() server()
configureForGameTest(this) configureForGameTest()
property("fabric-api.gametest") property("fabric-api.gametest")
property( property(
"fabric-api.gametest.report-file", "fabric-api.gametest.report-file",
layout.buildDirectory.dir("test-results/runGametest.xml") layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(),
.getAbsolutePath(),
) )
runDir("run/gametest") runDir("run/gametest")
} }
register("exampleClient") {
client()
configName = "Example Mod Client"
source(sourceSets.examples.get())
}
register("exampleData") {
configName = "Example Mod Datagen"
configureForData(sourceSets.examples.get())
source(sourceSets.examples.get())
}
} }
} }

View File

@ -0,0 +1,31 @@
package com.example.examplemod;
import com.example.examplemod.peripheral.BrewingStandPeripheral;
import dan200.computercraft.api.peripheral.PeripheralLookup;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
import net.fabricmc.api.ModInitializer;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.BlockEntityType;
/**
* The main entry point for our example mod.
*/
public class FabricExampleMod implements ModInitializer {
@Override
public void onInitialize() {
// @start region=turtle_upgrades
@SuppressWarnings("unchecked")
var turtleUpgradeSerialisers = (Registry<UpgradeType<? extends ITurtleUpgrade>>) BuiltInRegistries.REGISTRY.get(ITurtleUpgrade.typeRegistry().location());
Registry.register(turtleUpgradeSerialisers, ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade"), ExampleMod.EXAMPLE_TURTLE_UPGRADE);
// @end region=turtle_upgrades
ExampleMod.registerComputerCraft();
// @start region=peripherals
PeripheralLookup.get().registerForBlockEntity((f, s) -> new BrewingStandPeripheral(f), BlockEntityType.BREWING_STAND);
// @end region=peripherals
}
}

View File

@ -0,0 +1,14 @@
package com.example.examplemod;
import dan200.computercraft.api.client.FabricComputerCraftAPIClient;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import net.fabricmc.api.ClientModInitializer;
public class FabricExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// @start region=turtle_modellers
FabricComputerCraftAPIClient.registerTurtleUpgradeModeller(ExampleMod.EXAMPLE_TURTLE_UPGRADE, TurtleUpgradeModeller.flatItem());
// @end region=turtle_modellers
}
}

View File

@ -0,0 +1,49 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleUpgradeProvider;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider;
import net.fabricmc.fabric.api.event.registry.DynamicRegistries;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder;
import java.util.concurrent.CompletableFuture;
/**
* Data generators for our Fabric example mod.
*/
public class FabricExampleModDataGenerator implements DataGeneratorEntrypoint {
@Override
public void onInitializeDataGenerator(FabricDataGenerator generator) {
var pack = generator.createPack();
addTurtleUpgrades(pack, generator.getRegistries());
}
// @start region=turtle_upgrades
private static void addTurtleUpgrades(FabricDataGenerator.Pack pack, CompletableFuture<HolderLookup.Provider> registries) {
var fullRegistryPatch = TurtleUpgradeProvider.makeUpgradeRegistry(registries);
pack.addProvider((FabricDataOutput output) -> new AutomaticDynamicRegistryProvider(output, fullRegistryPatch));
}
/**
* A subclass of {@link FabricDynamicRegistryProvider} that writes all new entries.
*/
private static class AutomaticDynamicRegistryProvider extends FabricDynamicRegistryProvider {
AutomaticDynamicRegistryProvider(FabricDataOutput output, CompletableFuture<RegistrySetBuilder.PatchedRegistries> registries) {
super(output, registries.thenApply(RegistrySetBuilder.PatchedRegistries::patches));
}
@Override
protected void configure(HolderLookup.Provider registries, Entries entries) {
for (var r : DynamicRegistries.getDynamicRegistries()) entries.addAll(registries.lookupOrThrow(r.key()));
}
@Override
public String getName() {
return "Registries";
}
}
// @end region=turtle_upgrades
}

View File

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"id": "examplemod",
"version": "1.0.0",
"entrypoints": {
"main": [
"com.example.examplemod.FabricExampleMod"
],
"fabric-datagen": [
"com.example.examplemod.FabricExampleModDataGenerator"
]
},
"depends": {
"computercraft": "*"
}
}

View File

@ -7,10 +7,6 @@ plugins {
id("cc-tweaked.publishing") id("cc-tweaked.publishing")
} }
java {
withJavadocJar()
}
cct.inlineProject(":common-api") cct.inlineProject(":common-api")
dependencies { dependencies {

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.* import cc.tweaked.gradle.*
import net.neoforged.gradle.dsl.common.runs.run.Run import net.neoforged.moddevgradle.dsl.RunModel
plugins { plugins {
id("cc-tweaked.forge") id("cc-tweaked.forge")
@ -19,106 +19,122 @@ cct {
allProjects.forEach { externalSources(it) } allProjects.forEach { externalSources(it) }
} }
sourceSets { neoForge {
main { val computercraft by mods.registering {
resources.srcDir("src/generated/resources")
}
testMod { runs { modIdentifier = "cctest" } }
testFixtures { runs { modIdentifier = "cctest" } }
}
minecraft {
accessTransformers {
file("src/main/resources/META-INF/accesstransformer.cfg")
}
}
runs {
configureEach {
systemProperty("forge.logging.markers", "REGISTRIES")
systemProperty("forge.logging.console.level", "debug")
cct.sourceDirectories.get().forEach { cct.sourceDirectories.get().forEach {
if (it.classes) modSources.add("computercraft", it.sourceSet) if (it.classes) sourceSet(it.sourceSet)
}
dependencies {
runtime(configurations["minecraftLibrary"])
} }
} }
val client by registering { val computercraftDatagen by mods.registering {
workingDirectory(file("run")) cct.sourceDirectories.get().forEach {
} if (it.classes) sourceSet(it.sourceSet)
val server by registering {
workingDirectory(file("run/server"))
argument("--nogui")
}
val data by registering {
workingDirectory(file("run"))
arguments.addAll(
"--mod", "computercraft", "--all",
"--output", layout.buildDirectory.dir("generatedResources").getAbsolutePath(),
"--existing", project(":common").file("src/main/resources/").absolutePath,
"--existing", file("src/main/resources/").absolutePath,
)
modSources.add("computercraft", sourceSets.datagen.get())
}
fun Run.configureForGameTest() {
gameTest()
systemProperty("cctest.sources", project(":common").file("src/testMod/resources/data/cctest").absolutePath)
modSource(sourceSets.testMod.get())
modSource(sourceSets.testFixtures.get())
modSources.add("cctest", project(":core").sourceSets.testFixtures.get())
jvmArgument("-ea")
dependencies {
runtime(configurations["testMinecraftLibrary"])
} }
sourceSet(sourceSets.datagen.get())
} }
val gameTestServer by registering { val testMod by mods.registering {
workingDirectory(file("run/testServer")) sourceSet(sourceSets.testMod.get())
configureForGameTest() sourceSet(sourceSets.testFixtures.get())
sourceSet(project(":core").sourceSets["testFixtures"])
} }
val gameTestClient by registering { val exampleMod by mods.registering {
configure(runTypes.named("client")) sourceSet(sourceSets.examples.get())
}
workingDirectory(file("run/testClient")) runs {
configureForGameTest() configureEach {
ideName = "Forge - ${name.capitalise()}"
systemProperty("forge.logging.markers", "REGISTRIES")
systemProperty("forge.logging.console.level", "debug")
loadedMods.add(computercraft)
}
systemProperties("cctest.tags", "client,common") register("client") {
client()
}
register("server") {
server()
gameDirectory = file("run/server")
programArgument("--nogui")
}
fun RunModel.configureForData(mod: String, sourceSet: SourceSet) {
data()
gameDirectory = file("run/run${name.capitalise()}")
programArguments.addAll(
"--mod", mod, "--all",
"--output",
layout.buildDirectory.dir(sourceSet.getTaskName("generateResources", null))
.getAbsolutePath(),
"--existing", project.project(":common").file("src/${sourceSet.name}/resources/").absolutePath,
"--existing", project.file("src/${sourceSet.name}/resources/").absolutePath,
)
}
register("data") {
configureForData("computercraft", sourceSets.main.get())
loadedMods = listOf(computercraftDatagen.get())
}
fun RunModel.configureForGameTest() {
systemProperty(
"cctest.sources",
project.project(":common").file("src/testMod/resources/data/cctest").absolutePath,
)
programArgument("--mixin.config=computercraft-gametest.mixins.json")
loadedMods.add(testMod)
jvmArgument("-ea")
}
register("testClient") {
client()
gameDirectory = file("run/testClient")
configureForGameTest()
systemProperty("cctest.tags", "client,common")
}
register("gametest") {
type = "gameTestServer"
configureForGameTest()
systemProperty("forge.logging.console.level", "info")
systemProperty(
"cctest.gametest-report",
layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(),
)
gameDirectory = file("run/gametest")
}
register("exampleClient") {
client()
loadedMods.add(exampleMod.get())
}
register("exampleData") {
configureForData("examplemod", sourceSets.examples.get())
loadedMods.add(exampleMod.get())
}
} }
} }
configurations { configurations {
val minecraftEmbed by registering { additionalRuntimeClasspath { extendsFrom(jarJar.get()) }
isCanBeResolved = false
isCanBeConsumed = false
}
named("jarJar") { extendsFrom(minecraftEmbed.get()) }
val minecraftLibrary by registering { val testAdditionalRuntimeClasspath by registering {
isCanBeResolved = true
isCanBeConsumed = false
extendsFrom(minecraftEmbed.get())
}
runtimeOnly { extendsFrom(minecraftLibrary.get()) }
val testMinecraftLibrary by registering {
isCanBeResolved = true isCanBeResolved = true
isCanBeConsumed = false isCanBeConsumed = false
// Prevent ending up with multiple versions of libraries on the classpath. // Prevent ending up with multiple versions of libraries on the classpath.
shouldResolveConsistentlyWith(minecraftLibrary.get()) shouldResolveConsistentlyWith(additionalRuntimeClasspath.get())
}
for (testConfig in listOf("testClientAdditionalRuntimeClasspath", "gametestAdditionalRuntimeClasspath")) {
named(testConfig) { extendsFrom(testAdditionalRuntimeClasspath.get()) }
} }
register("testWithIris") { register("testWithIris") {
@ -136,21 +152,18 @@ dependencies {
runtimeOnly(libs.bundles.externalMods.forge.runtime) { cct.exclude(this) } runtimeOnly(libs.bundles.externalMods.forge.runtime) { cct.exclude(this) }
compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false } compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false }
implementation("net.neoforged:neoforge:${libs.versions.neoForge.get()}")
// Depend on our other projects. // Depend on our other projects.
api(commonClasses(project(":forge-api"))) { cct.exclude(this) } api(commonClasses(project(":forge-api"))) { cct.exclude(this) }
clientApi(clientClasses(project(":forge-api"))) { cct.exclude(this) } clientApi(clientClasses(project(":forge-api"))) { cct.exclude(this) }
implementation(project(":core")) { cct.exclude(this) } implementation(project(":core")) { cct.exclude(this) }
"minecraftEmbed"(libs.cobalt) jarJar(libs.cobalt)
"minecraftEmbed"(libs.jzlib) jarJar(libs.jzlib)
// We don't jar-in-jar our additional netty dependencies (see the tasks.jarJar configuration), but still want them // We don't jar-in-jar our additional netty dependencies (see the tasks.jarJar configuration), but still want them
// on the legacy classpath. // on the legacy classpath.
"minecraftLibrary"(libs.netty.http) additionalRuntimeClasspath(libs.netty.http) { isTransitive = false }
"minecraftLibrary"(libs.netty.socks) additionalRuntimeClasspath(libs.netty.socks) { isTransitive = false }
"minecraftLibrary"(libs.netty.proxy) additionalRuntimeClasspath(libs.netty.proxy) { isTransitive = false }
testFixturesApi(libs.bundles.test) testFixturesApi(libs.bundles.test)
testFixturesApi(libs.bundles.kotlin) testFixturesApi(libs.bundles.kotlin)
@ -163,8 +176,8 @@ dependencies {
testModImplementation(testFixtures(project(":forge"))) testModImplementation(testFixtures(project(":forge")))
// Ensure our test fixture dependencies are on the classpath // Ensure our test fixture dependencies are on the classpath
"testMinecraftLibrary"(libs.bundles.kotlin) "testAdditionalRuntimeClasspath"(libs.bundles.kotlin)
"testMinecraftLibrary"(libs.bundles.test) "testAdditionalRuntimeClasspath"(libs.bundles.test)
testFixturesImplementation(testFixtures(project(":core"))) testFixturesImplementation(testFixtures(project(":core")))
@ -178,15 +191,12 @@ tasks.processResources {
inputs.property("modVersion", modVersion) inputs.property("modVersion", modVersion)
inputs.property("neoVersion", libs.versions.neoForge.get()) inputs.property("neoVersion", libs.versions.neoForge.get())
filesMatching("META-INF/neoforge.mods.toml") { filesMatching("META-INF/mods.toml") {
expand(mapOf("neoVersion" to libs.versions.neoForge.get(), "file" to mapOf("jarVersion" to modVersion))) expand(mapOf("neoVersion" to libs.versions.neoForge.get(), "file" to mapOf("jarVersion" to modVersion)))
} }
} }
tasks.jar { tasks.jar {
archiveClassifier.set("slim")
duplicatesStrategy = DuplicatesStrategy.FAIL
// Include all classes from other projects except core. // Include all classes from other projects except core.
val coreSources = project(":core").sourceSets["main"] val coreSources = project(":core").sourceSets["main"]
for (source in cct.sourceDirectories.get()) { for (source in cct.sourceDirectories.get()) {
@ -203,42 +213,28 @@ tasks.sourcesJar {
for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource) for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource)
} }
tasks.jarJar {
archiveClassifier.set("")
}
tasks.assemble { dependsOn("jarJar") }
// Check tasks // Check tasks
tasks.test { tasks.test {
systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath()) systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath())
} }
val runGametest by tasks.registering(JavaExec::class) { val runGametest = tasks.named<JavaExec>("runGametest") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Runs tests on a temporary Minecraft instance."
dependsOn("cleanRunGametest")
usesService(MinecraftRunnerService.get(gradle)) usesService(MinecraftRunnerService.get(gradle))
copyFromTask("runGameTestServer")
systemProperty("forge.logging.console.level", "info")
systemProperty("cctest.gametest-report", layout.buildDirectory.dir("test-results/$name.xml").getAbsolutePath())
} }
cct.jacoco(runGametest) cct.jacoco(runGametest)
tasks.check { dependsOn(runGametest) } tasks.check { dependsOn(runGametest) }
val runGametestClient by tasks.registering(ClientJavaExec::class) { val runGametestClient by tasks.registering(ClientJavaExec::class) {
description = "Runs client-side gametests with no mods" description = "Runs client-side gametests with no mods"
copyFrom("runGameTestClient") copyFromForge("runTestClient")
tags("client") tags("client")
} }
cct.jacoco(runGametestClient) cct.jacoco(runGametestClient)
val runGametestClientWithIris by tasks.registering(ClientJavaExec::class) { val runGametestClientWithIris by tasks.registering(ClientJavaExec::class) {
description = "Runs client-side gametests with Iris" description = "Runs client-side gametests with Iris"
copyFrom("runGameTestClient") copyFromForge("runGameTestClient")
tags("iris") tags("iris")
classpath += configurations["testWithIris"] classpath += configurations["testWithIris"]
@ -256,20 +252,15 @@ tasks.register("checkClient") {
// Upload tasks // Upload tasks
modPublishing { modPublishing {
output.set(tasks.jarJar) output.set(tasks.jar)
} }
// Don't publish the slim jar
for (cfg in listOf(configurations.apiElements, configurations.runtimeElements)) {
cfg.configure { artifacts.removeIf { it.classifier == "slim" } }
}
tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false }
publishing { publishing {
publications { publications {
named("maven", MavenPublication::class) { named("maven", MavenPublication::class) {
mavenDependencies { mavenDependencies {
cct.configureExcludes(this) cct.configureExcludes(this)
exclude(libs.jei.forge.get())
} }
} }
} }

View File

@ -0,0 +1,41 @@
package com.example.examplemod;
import com.example.examplemod.peripheral.BrewingStandPeripheral;
import dan200.computercraft.api.peripheral.PeripheralCapability;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import net.neoforged.neoforge.registries.RegisterEvent;
/**
* The main entry point for the Forge version of our example mod.
*/
@Mod(ExampleMod.MOD_ID)
public class ForgeExampleMod {
public ForgeExampleMod(IEventBus modBus) {
// Register our turtle upgrade. If writing a Forge-only mod, you'd normally use DeferredRegister instead.
// However, this is an easy way to implement this in a multi-loader-compatible manner.
// @start region=turtle_upgrades
modBus.addListener((RegisterEvent event) -> {
event.register(
ITurtleUpgrade.typeRegistry(),
ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade"),
() -> ExampleMod.EXAMPLE_TURTLE_UPGRADE
);
});
// @end region=turtle_upgrades
modBus.addListener((FMLCommonSetupEvent event) -> ExampleMod.registerComputerCraft());
// @start region=peripherals
modBus.addListener((RegisterCapabilitiesEvent event) -> {
event.registerBlockEntity(PeripheralCapability.get(), BlockEntityType.BREWING_STAND, (b, d) -> new BrewingStandPeripheral(b));
});
// @end region=peripherals
}
}

View File

@ -0,0 +1,20 @@
package com.example.examplemod;
import dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
/**
* The client-side entry point for the Forge version of our example mod.
*/
@EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD)
public class ForgeExampleModClient {
// @start region=turtle_modellers
@SubscribeEvent
public static void onRegisterTurtleModellers(RegisterTurtleModellersEvent event) {
event.register(ExampleMod.EXAMPLE_TURTLE_UPGRADE, TurtleUpgradeModeller.flatItem());
}
// @end region=turtle_modellers
}

View File

@ -0,0 +1,31 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleUpgradeProvider;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.DataGenerator;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.common.data.DatapackBuiltinEntriesProvider;
import net.neoforged.neoforge.data.event.GatherDataEvent;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
/**
* Data generators for the Forge version of our example mod.
*/
@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
public class ForgeExampleModDataGenerator {
@SubscribeEvent
public static void gather(GatherDataEvent event) {
var pack = event.getGenerator().getVanillaPack(true);
addTurtleUpgrades(pack, event.getLookupProvider());
}
// @start region=turtle_upgrades
private static void addTurtleUpgrades(DataGenerator.PackGenerator pack, CompletableFuture<HolderLookup.Provider> registries) {
var fullRegistryPatch = TurtleUpgradeProvider.makeUpgradeRegistry(registries);
pack.addProvider(o -> new DatapackBuiltinEntriesProvider(o, fullRegistryPatch, Set.of(ExampleMod.MOD_ID)));
}
// @end region=turtle_upgrades
}

View File

@ -0,0 +1,14 @@
modLoader="javafml"
loaderVersion="[1,)"
license="CC0-1.0"
[[mods]]
modId="examplemod"
version="1.0.0"
[[dependencies.examplemod]]
modId="computercraft"
mandatory=true
versionRange="[1.0,)"
ordering="AFTER"
side="BOTH"

View File

@ -0,0 +1,6 @@
{
"pack": {
"pack_format": 15,
"description": "Example Mod"
}
}

View File

@ -474,4 +474,4 @@
"count": 1 "count": 1
} }
} }
} }

View File

@ -8,14 +8,10 @@ pluginManagement {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven("https://maven.neoforged.net/releases") { maven("https://maven.neoforged.net") {
name = "NeoForge" name = "NeoForge"
content { content {
includeGroup("net.minecraftforge")
includeGroup("net.neoforged") includeGroup("net.neoforged")
includeGroup("net.neoforged.gradle")
includeModule("codechicken", "DiffPatch")
includeModule("net.covers1624", "Quack")
} }
} }