1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-02-02 20:29:13 +00:00

Clean up Javadocs a little

I've no motivation for modding right now, but always got time for build
system busywork!

CC:T (and CC before that) has always published its API docs. However,
they're not always the most helpful — they're useful if you know what
you're looking for, but aren't a good getting-started guide.

Part of the issue here is there's no examples, and everything is
described pretty abstractly. I have occasionally tried to improve this
(e.g. the peripheral docs in bdffabc08e),
but it's a long road.

This commit adds a new example mod, which registers peripherals, an API
and a turtle upgrade. While the mod itself isn't exported as part of the
docs, we reference blocks of it using Java's new {@snippet} tag.

 - Switch the Forge project to use NeoForge's new Legacy MDG plugin. We
   don't *need* to do this, but it means the build logic for Forge and
   NeoForge is more closely aligned.

 - Add a new SnippetTaglet, which is a partial backport of Java 18+'s
   {@snippet}.

 - Add an example mod. This is a working multi-loader mod, complete with
   datagen (albeit with no good multi-loader abstractions).

 - Move our existing <pre>{@code ...}</pre> blocks into the example mod,
   replacing them with {@snippet}s.

 - Add a new overview page to the docs, providing some getting-started
   information. We had this already in the dan200.computercraft.api
   package docs, but it's not especially visible there.
This commit is contained in:
Jonathan Coates 2025-01-09 20:47:51 +00:00
parent d9fc1c3a80
commit 3c46b8acd7
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
57 changed files with 1089 additions and 616 deletions

View File

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

View File

@ -61,19 +61,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
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!

View File

@ -8,10 +8,10 @@ SPDX-PackageSupplier = "Jonathan Coates <git@squiddev.cc>"
SPDX-PackageDownloadLocation = "https://github.com/cc-tweaked/cc-tweaked"
[[annotations]]
# Generated/data files are CC0.
SPDX-FileCopyrightText = "The CC: Tweaked Developers"
SPDX-License-Identifier = "CC0-1.0"
path = [
# Generated/data files are CC0.
"gradle/gradle-daemon-jvm.properties",
"projects/common/src/main/resources/assets/computercraft/sounds.json",
"projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg",
@ -20,6 +20,11 @@ path = [
"projects/**/src/generated/**",
"projects/web/src/htmlTransform/export/index.json",
"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]]
@ -46,7 +51,6 @@ path = [
"projects/fabric/src/main/resources/fabric.mod.json",
"projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json",
"projects/fabric/src/testMod/resources/fabric.mod.json",
"projects/forge/src/client/resources/computercraft-client.forge.mixins.json",
"projects/web/src/frontend/mount/.settings",
"projects/web/src/frontend/mount/example.nfp",
"projects/web/src/frontend/mount/example.nft",
@ -73,7 +77,7 @@ path = [
]
[[annotations]]
# Community-contributed license files
# Community-contributed language files
SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers"
SPDX-License-Identifier = "LicenseRef-CCPL"
path = [
@ -87,18 +91,11 @@ path = [
]
[[annotations]]
# Community-contributed license files
# Community-contributed language files
SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers"
SPDX-License-Identifier = "MPL-2.0"
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]]
path = ["gradle/wrapper/**"]
SPDX-FileCopyrightText = "Gradle Inc"

View File

@ -14,18 +14,10 @@ repositories {
mavenCentral()
gradlePluginPortal()
maven("https://maven.minecraftforge.net") {
name = "Forge"
maven("https://maven.neoforged.net") {
name = "NeoForge"
content {
includeGroup("net.minecraftforge")
includeGroup("net.minecraftforge.gradle")
}
}
maven("https://maven.parchmentmc.org") {
name = "Librarian"
content {
includeGroupByRegex("^org\\.parchmentmc.*")
includeGroup("net.neoforged")
}
}
@ -50,10 +42,9 @@ dependencies {
implementation(libs.spotless)
implementation(libs.fabric.loom)
implementation(libs.forgeGradle)
implementation(libs.ideaExt)
implementation(libs.librarian)
implementation(libs.minotaur)
implementation(libs.modDevGradle)
implementation(libs.vanillaExtract)
}

View File

@ -10,21 +10,22 @@ import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("net.minecraftforge.gradle")
// We must apply java-convention after Forge, as we need the fg extension to be present.
id("cc-tweaked.java-convention")
id("org.parchmentmc.librarian.forgegradle")
id("net.neoforged.moddev.legacyforge")
}
plugins.apply(CCTweakedPlugin::class.java)
val mcVersion: String by extra
minecraft {
legacyForge {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
mappings("parchment", "${libs.findVersion("parchmentMc").get()}-${libs.findVersion("parchment").get()}-$mcVersion")
version = "${mcVersion}-${libs.findVersion("forge").get()}"
accessTransformer(project(":forge").file("src/main/resources/META-INF/accesstransformer.cfg"))
parchment {
minecraftVersion = libs.findVersion("parchmentMc").get().toString()
mappingsVersion = libs.findVersion("parchment").get().toString()
}
}
MinecraftConfigurations.setup(project)
@ -32,13 +33,3 @@ MinecraftConfigurations.setup(project)
extensions.configure(CCTweakedExtension::class.java) {
linters(minecraft = true, loader = "forge")
}
dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
"minecraft"("net.minecraftforge:forge:$mcVersion-${libs.findVersion("forge").get()}")
}
tasks.configureEach {
// genIntellijRuns isn't registered until much later, so we need this silly hijinks.
if (name == "genIntellijRuns") doLast { IdeaRunConfigurations(project).patch() }
}

View File

@ -44,13 +44,6 @@ repositories {
exclusiveContent {
forRepositories(mainMaven)
// Include the ForgeGradle repository if present. This requires that ForgeGradle is already present, which we
// enforce in our Forge overlay.
val fg =
project.extensions.findByType(net.minecraftforge.gradle.userdev.DependencyManagementExtension::class.java)
if (fg != null) forRepositories(fg.repository)
filter {
includeGroup("cc.tweaked")
// Things we mirror
@ -99,6 +92,7 @@ sourceSets.all {
check("OperatorPrecedence", CheckSeverity.OFF) // For now.
check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid
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)
option(

View File

@ -2,15 +2,16 @@
//
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
/**
* Sets up the configurations for writing game tests.
*
* 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 {
id("cc-tweaked.kotlin-convention")
id("cc-tweaked.java-convention")
@ -19,33 +20,16 @@ plugins {
val main = sourceSets["main"]
val client = sourceSets["client"]
// datagen and testMod inherit from the main and client classpath, just so we have access to Minecraft classes.
val datagen by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
}
MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.DATAGEN)
MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.EXAMPLES)
MinecraftConfigurations.createDerivedConfiguration(project, MinecraftConfigurations.TEST_MOD)
val testMod by sourceSets.creating {
compileClasspath += main.compileClasspath + client.compileClasspath
runtimeClasspath += main.runtimeClasspath + client.runtimeClasspath
}
// Set up generated resources
sourceSets.main { resources.srcDir("src/generated/resources") }
sourceSets.named("examples") { resources.srcDir("src/examples/generatedResources") }
val extraConfigurations = listOf(datagen, testMod)
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)
}
}
// Make sure our examples compile.
tasks.check { dependsOn(tasks.named("compileExamplesJava")) }
// 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.
extendSourceSet(otherProject, main)
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) }
}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import net.minecraftforge.gradle.common.util.RunConfig
import net.minecraftforge.gradle.common.util.runs.setRunConfigInternal
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.JavaExec
import org.gradle.jvm.toolchain.JavaToolchainService
import java.nio.file.Files
/**
* Set [JavaExec] task to run a given [RunConfig].
*/
fun JavaExec.setRunConfig(config: RunConfig) {
dependsOn("prepareRuns")
setRunConfigInternal(project, this, config)
doFirst("Create working directory") { Files.createDirectories(workingDir.toPath()) }
javaLauncher.set(
project.extensions.getByType(JavaToolchainService::class.java)
.launcherFor(project.extensions.getByType(JavaPluginExtension::class.java).toolchain),
)
}

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 sourceSets = java.sourceSets
private val configurations = project.configurations
private val objects = project.objects
private val main = sourceSets[SourceSet.MAIN_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")
// Ensure the client classpaths behave the same as the main ones.
configurations.named(client.compileClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.compileClasspathConfigurationName])
}
configurations.named(client.runtimeClasspathConfigurationName) {
shouldResolveConsistentlyWith(configurations[main.runtimeClasspathConfigurationName])
}
consistentWithMain(client)
// Set up an API configuration for clients (to ensure it's consistent with the main source set).
val clientApi = configurations.maybeCreate(client.apiConfigurationName).apply {
@ -85,6 +78,16 @@ class MinecraftConfigurations private constructor(private val project: Project)
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() {
val client = sourceSets["client"]
@ -102,7 +105,24 @@ class MinecraftConfigurations private constructor(private val project: Project)
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 {
const val DATAGEN = "datagen"
const val EXAMPLES = "examples"
const val TEST_MOD = "testMod"
fun setupBasic(project: Project) {
MinecraftConfigurations(project).setupBasic()
}
@ -110,6 +130,10 @@ class MinecraftConfigurations private constructor(private val project: Project)
fun setup(project: Project) {
MinecraftConfigurations(project).setup()
}
fun createDerivedConfiguration(project: Project, name: String) {
MinecraftConfigurations(project).createDerivedConfiguration(name)
}
}
}

View File

@ -4,7 +4,7 @@
package cc.tweaked.gradle
import net.minecraftforge.gradle.common.util.RunConfig
import net.neoforged.moddevgradle.internal.RunGameTask
import org.gradle.api.GradleException
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.invocation.Gradle
@ -65,11 +65,19 @@ abstract class ClientJavaExec : JavaExec() {
setTestProperties()
}
fun copyFromForge(path: String) = copyFromForge(project.tasks.getByName(path, RunGameTask::class))
/**
* Set this task to run a given [RunConfig].
* Set this task to run a given [RunGameTask].
*/
fun setRunConfig(config: RunConfig) {
(this as JavaExec).setRunConfig(config)
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.
}

View File

@ -1,51 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package net.minecraftforge.gradle.common.util.runs
import net.minecraftforge.gradle.common.util.RunConfig
import org.gradle.api.Project
import org.gradle.process.CommandLineArgumentProvider
import org.gradle.process.JavaExecSpec
import java.io.File
import java.util.function.Supplier
import java.util.stream.Collectors
import java.util.stream.Stream
/**
* Set up a [JavaExecSpec] to execute a [RunConfig].
*
* [MinecraftRunTask] sets up all its properties when the task is executed, rather than when configured. As such, it's
* not possible to use [cc.tweaked.gradle.copyToFull] like we do for Fabric. Instead, we set up the task manually.
*
* Unfortunately most of the functionality we need is package-private, and so we have to put our code into the package.
*/
internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: RunConfig) {
spec.workingDir = File(config.workingDirectory)
spec.mainClass.set(config.main)
for (source in config.allSources) spec.classpath(source.runtimeClasspath)
val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java)
// Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts.
val lazyTokens = RunConfigGenerator.configureTokensLazy(
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
originalTask.get().minecraftArtifacts,
originalTask.get().runtimeClasspathArtifacts,
)
spec.argumentProviders.add(
CommandLineArgumentProvider {
RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()
},
)
spec.jvmArgumentProviders.add(
CommandLineArgumentProvider {
(if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } +
config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" }
},
)
for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value))
}

View File

@ -124,7 +124,7 @@ SPDX-License-Identifier: MPL-2.0
</module>
<module name="MethodTypeParameterName" />
<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 name="ParameterName" />
<module name="StaticVariableName">

View File

@ -61,14 +61,13 @@ checkstyle = "10.14.1"
errorProne-core = "2.27.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.7.1"
forgeGradle = "6.0.21"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
illuaminate = "0.1.0-74-gf1551d5"
librarian = "1.+"
lwjgl = "3.3.3"
minotaur = "2.+"
minotaur = "2.8.7"
modDevGradle = "2.0.74"
nullAway = "0.10.25"
shadow = "8.3.1"
spotless = "6.23.3"
@ -150,12 +149,11 @@ errorProne-core = { module = "com.google.errorprone:error_prone_core", version.r
errorProne-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorProne-plugin" }
errorProne-testHelpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "errorProne-core" }
fabric-loom = { module = "net.fabricmc:fabric-loom", version.ref = "fabric-loom" }
forgeGradle = { module = "net.minecraftforge.gradle:ForgeGradle", version.ref = "forgeGradle" }
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" }
librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" }
minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" }
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" }
teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" }
teavm-core = { module = "org.teavm:teavm-core", version.ref = "teavm" }
@ -170,11 +168,9 @@ vanillaExtract = { module = "cc.tweaked.vanilla-extract:plugin", version.ref = "
yarn = { module = "net.fabricmc:yarn", version.ref = "yarn" }
[plugins]
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" }
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }

View File

@ -18,13 +18,28 @@ dependencies {
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 {
title = "CC: Tweaked $version Minecraft $mcVersion"
title = "CC: Tweaked $version for Minecraft $mcVersion"
include("dan200/computercraft/api/**/*.java")
options {
(this as StandardJavadocDocletOptions)
inputs.files(javadocOverview)
overview(javadocOverview.get().destinationDir.resolve("overview.html").absolutePath)
groups = mapOf(
"Common" to listOf(
"dan200.computercraft.api",
@ -47,6 +62,15 @@ tasks.javadoc {
<link href=" https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css " rel="stylesheet">
""".trimIndent(),
)
taglets("cc.tweaked.javadoc.SnippetTaglet")
tagletPath(configurations.detachedConfiguration(dependencies.project(":lints")).toList())
val snippetSources = listOf(":common", ":fabric", ":forge").flatMap {
project(it).sourceSets["examples"].allSource.sourceDirectories
}
inputs.files(snippetSources)
jFlags("-Dcc.snippet-path=" + snippetSources.joinToString(File.pathSeparator) { it.absolutePath })
}
// Include the core-api in our javadoc export. This is wrong, but it means we can export a single javadoc dump.

View File

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

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.
* <p>
* 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:
*
* <pre class="language language-java">{@code
* 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);
* });
* }</pre>
* {@snippet class=com.example.examplemod.ExampleAPI region=register}
*
* @param factory The factory for your API subclass.
* @see ILuaAPIFactory

View File

@ -14,13 +14,10 @@ import javax.annotation.Nullable;
* A peripheral which can be equipped to the back side of a pocket computer.
* <p>
* Pocket upgrades are defined in two stages. First, on creates a {@link IPocketUpgrade} subclass and corresponding
* {@link PocketUpgradeSerialiser} instance, which are then registered in a Forge registry.
* {@link PocketUpgradeSerialiser} instance, which are then registered in a Minecraft registry.
* <p>
* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and
* the upgrade registered internally. See the documentation in {@link PocketUpgradeSerialiser} for details on this process
* and where files should be located.
*
* @see PocketUpgradeSerialiser For how to register a pocket computer upgrade.
* the upgrade registered internally.
*/
public interface IPocketUpgrade extends UpgradeBase {
/**

View File

@ -8,21 +8,69 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.upgrades.UpgradeBase;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.Items;
import javax.annotation.Nullable;
import java.util.function.BiFunction;
/**
* The primary interface for defining an update for Turtles. A turtle update can either be a new tool, or a new
* peripheral.
* <p>
* Turtle upgrades are defined in two stages. First, one creates a {@link ITurtleUpgrade} subclass and corresponding
* {@link TurtleUpgradeSerialiser} instance, which are then registered in a Forge registry.
* {@link TurtleUpgradeSerialiser} instance, which are then registered in a Minecraft registry.
* <p>
* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and
* the upgrade registered internally. See the documentation in {@link TurtleUpgradeSerialiser} for details on this process
* and where files should be located.
* the upgrade automatically registered.
*
* @see TurtleUpgradeSerialiser For how to register a turtle upgrade.
* <h2>Example</h2>
* <h3>Registering the upgrade serialiser</h3>
* First, let's create a new class that implements {@link ITurtleUpgrade}. It is recommended to subclass
* {@link AbstractTurtleUpgrade}, as that provides a default implementation of most methods.
* <p>
* {@snippet class=com.example.examplemod.ExampleTurtleUpgrade region=body}
* <p>
* Now we must construct a new upgrade serialiser. In most cases, you can use one of the helper methods
* (e.g. {@link TurtleUpgradeSerialiser#simpleWithCustomItem(BiFunction)}), rather than defining your own implementation.
*
* {@snippet class=com.example.examplemod.ExampleMod region=turtle_upgrades}
*
* We now must register this upgrade serialiser. 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 serialiser.
*
* <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>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_upgrades/<my_upgrade_id>.json}.
*
* {@snippet file = data/examplemod/computercraft/turtle_upgrades/example_turtle_upgrade.json}
*
* The {@code "type"} field points to the ID of the upgrade serialiser we've just registered, while the other fields
* are read by the serialiser itself. As our upgrade was defined with {@link TurtleUpgradeSerialiser#simpleWithCustomItem(BiFunction)}, the
* {@code "item"} field will construct our upgrade with {@link Items#COMPASS}.
* <p>
* Rather than manually creating the file, it is recommended to data-generators to generate this file. This can be done
* with {@link TurtleUpgradeDataProvider}.
*
* {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body}
*
* @see TurtleUpgradeSerialiser Registering a turtle upgrade.
*/
public interface ITurtleUpgrade extends UpgradeBase {
/**

View File

@ -29,6 +29,9 @@ import java.util.function.Consumer;
* {@link #addUpgrades(Consumer)} function, construct each upgrade, and pass them off to the provided consumer to
* generate them.
*
* <h2>Example</h2>
* {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body}
*
* @see TurtleUpgradeSerialiser
*/
public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITurtleUpgrade, TurtleUpgradeSerialiser<?>> {

View File

@ -27,32 +27,6 @@ import java.util.function.Function;
* If your turtle upgrade doesn't have any associated configurable parameters (like most upgrades), you can use
* {@link #simple(Function)} or {@link #simpleWithCustomItem(BiFunction)} to create a basic upgrade serialiser.
*
* <h2>Example (Forge)</h2>
* <pre class="language language-java">{@code
* static final DeferredRegister<TurtleUpgradeSerialiser<?>> SERIALISERS = DeferredRegister.create( TurtleUpgradeSerialiser.TYPE, "my_mod" );
*
* // Register a new upgrade serialiser called "my_upgrade".
* public static final RegistryObject<TurtleUpgradeSerialiser<MyUpgrade>> MY_UPGRADE =
* SERIALISERS.register( "my_upgrade", () -> TurtleUpgradeSerialiser.simple( MyUpgrade::new ) );
*
* // Then in your constructor
* SERIALISERS.register( bus );
* }</pre>
* <p>
* We can then define a new upgrade using JSON by placing the following in
* {@literal data/<my_mod>/computercraft/turtle_upgrades/<my_upgrade_id>.json}}.
*
* <pre class="language language-json">{@code
* {
* "type": "my_mod:my_upgrade",
* }
* }</pre>
* <p>
* Finally, we need to register a model for our upgrade. The way to do this varies on mod loader, see
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information.
* <p>
* {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files.
*
* @param <T> The type of turtle upgrade this is responsible for serialising.
* @see ITurtleUpgrade
* @see TurtleUpgradeDataProvider

View File

@ -32,6 +32,8 @@ import java.util.function.Function;
*
* @param <T> The base class of upgrades.
* @param <R> The upgrade serialiser to register for.
* @see dan200.computercraft.api.turtle.TurtleUpgradeDataProvider
* @see dan200.computercraft.api.pocket.PocketUpgradeDataProvider
*/
public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends UpgradeSerialiser<? extends T>> implements DataProvider {
private final PackOutput output;
@ -84,13 +86,9 @@ public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends Upgra
/**
* Add all turtle or pocket computer upgrades.
* <p>
* <strong>Example usage:</strong>
* <pre class="language language-java">{@code
* protected void addUpgrades(Consumer<Upgrade<TurtleUpgradeSerialiser<?>>> addUpgrade) {
* simple(new ResourceLocation("mymod", "speaker"), SPEAKER_SERIALISER.get()).add(addUpgrade);
* }
* }</pre>
*
* <h4>Example</h4>
* {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body}
*
* @param addUpgrade A callback used to register an upgrade.
*/

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")
}
sourceSets {
main {
resources.srcDir("src/generated/resources")
}
}
minecraft {
accessWideners(
"src/main/resources/computercraft.accesswidener",
@ -113,20 +107,28 @@ val lintLua by tasks.registering(IlluaminateExec::class) {
doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
}
val runData by tasks.registering(MergeTrees::class) {
output = layout.projectDirectory.dir("src/generated/resources")
fun MergeTrees.configureForDatagen(source: SourceSet, outputFolder: String) {
output = layout.projectDirectory.dir(outputFolder)
for (loader in listOf("forge", "fabric")) {
mustRunAfter(":$loader:runData")
mustRunAfter(":$loader:$name")
source {
input {
from(project(":$loader").layout.buildDirectory.dir("generatedResources"))
from(project(":$loader").layout.buildDirectory.dir(source.getTaskName("generateResources", null)))
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 }

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.TurtleDataProvider;
import com.example.examplemod.peripheral.FurnacePeripheral;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
/**
* 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 serialiser for our example turtle upgrade. See the documentation for {@link TurtleUpgradeSerialiser}
* or {@code FabricExampleMod}/{@code ForgeExampleMod} for how this is registered.
* <p>
* This only defines the upgrade type. See {@link TurtleDataProvider} for defining the actual upgrade.
*/
// @start region=turtle_upgrades
public static final TurtleUpgradeSerialiser<ExampleTurtleUpgrade> EXAMPLE_TURTLE_UPGRADE = TurtleUpgradeSerialiser.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,17 @@
package com.example.examplemod;
import dan200.computercraft.api.turtle.AbstractTurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeType;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/**
* An example turtle upgrade.
*/
// @start region=body
public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade {
public ExampleTurtleUpgrade(ResourceLocation id, ItemStack stack) {
super(id, TurtleUpgradeType.PERIPHERAL, stack);
}
}
// @end region=body

View File

@ -0,0 +1,17 @@
package com.example.examplemod.data;
import net.minecraft.data.DataGenerator;
import net.minecraft.data.DataProvider;
/**
* The entry point to example mod's data-generators.
* <p>
* This is called by our platform-specific entry-point (see {@code FabricExampleModDataGenerator} and
* {@code ForgeExampleModDataGenerator}. That said, the exact setup isn't relevant (it will vary depending on
* mod-loader), what's interesting is the contents of the {@link #run(DataGenerator.PackGenerator)} method!
*/
public final class ExampleModDataGenerators {
public static void run(DataGenerator.PackGenerator pack) {
pack.addProvider((DataProvider.Factory<?>) TurtleDataProvider::new);
}
}

View File

@ -0,0 +1,34 @@
package com.example.examplemod.data;
import com.example.examplemod.ExampleMod;
import com.example.examplemod.ExampleTurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;
import java.util.function.Consumer;
/**
* A {@link TurtleUpgradeDataProvider} that generates the JSON for our {@linkplain ExampleTurtleUpgrade example
* upgrade}.
*
* @see ExampleModDataGenerators
*/
// @start region=body
public class TurtleDataProvider extends TurtleUpgradeDataProvider {
public TurtleDataProvider(PackOutput output) {
super(output);
}
@Override
protected void addUpgrades(Consumer<Upgrade<TurtleUpgradeSerialiser<?>>> addUpgrade) {
simpleWithCustomItem(
new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"),
ExampleMod.EXAMPLE_TURTLE_UPGRADE,
Items.COMPASS
).add(addUpgrade);
}
}
// @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().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 new ResourceLocation(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().getInt("BurnTime");
}
}
// @end region=body

View File

@ -8,10 +8,6 @@ plugins {
id("cc-tweaked")
}
java {
withJavadocJar()
}
// Due to the slightly circular nature of our API, add the main API jars to the javadoc classpath.
val docApi by configurations.registering {
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
* the given type, rather than requiring an exact type.
*
* <h2>Example:</h2>
* <pre class="language language-java">{@code
* @LuaFunction
* public final void doSomething(Coerced<String> myString) {
* var value = myString.value();
* }
* }</pre>
* <h2>Example</h2>
* {@snippet class=com.example.examplemod.ExampleAPI region=coerced}
*
* @param value The argument 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
* determined by their id, rather than any peripheral provider, though additional types may be provided by overriding
* {@link GenericPeripheral#getType()}.
* <p>
* For example, the main CC: Tweaked mod defines a generic source for inventories, which works on {@code IItemHandler}s:
*
* <pre class="language language-java">{@code
* public class InventoryMethods implements GenericSource {
* @LuaFunction(mainThread = true)
* public int size(IItemHandler inventory) {
* return inventory.getSlots();
* }
*
* // ...
* }
* }</pre>
* <h2>Example</h2>
* {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body}
* <p>
* New capabilities or block lookups (those not built into Forge/Fabric) must be explicitly registered using the
* loader-specific API.
* New capabilities (those not built into Forge) must be explicitly registered using the loader-specific API.
*
* @see dan200.computercraft.api.ComputerCraftAPI#registerGenericSource(GenericSource)
*/

View File

@ -4,17 +4,6 @@
/**
* 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 = {
TypeUseLocation.RETURN,

View File

@ -27,21 +27,7 @@ import java.util.function.Consumer;
*
* <h2>Example</h2>
*
* <pre class="language language-java">{@code
* 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);
* }
* }
* }</pre>
* {@snippet class=com.example.examplemod.peripheral.ComputerTrackingPeripheral region=body}
*
* @see IComputerAccess
* @see IPeripheral#attach(IComputerAccess)

View File

@ -48,35 +48,11 @@
* 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.
*
* <pre class="language language-java">{@code
* import dan200.computercraft.api.lua.LuaFunction;
* import dan200.computercraft.api.peripheral.GenericPeripheral;
* import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
* {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body}
*
* public final class FurnacePeripheral implements GenericPeripheral {
* @Override
* public String id() {
* return "mymod:furnace";
* }
*
* @LuaFunction(mainThread = true)
* public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
* return furnace.litTime;
* }
* }
* }</pre>
* <p>
* Finally, we need to register our peripheral, so that ComputerCraft is aware of it:
*
* <pre class="language language-java">{@code
* import dan200.computercraft.api.ComputerCraftAPI;
*
* public class ComputerCraftCompat {
* public static void register() {
* ComputerCraftAPI.registerGenericSource(new FurnacePeripheral());
* }
* }
* }</pre>
* {@snippet class=com.example.examplemod.ExampleMod region=generic_source}
*
* <h3>Creating a {@code IPeripheral}</h3>
* First, we'll need to create a new class that implements {@link dan200.computercraft.api.peripheral.IPeripheral}. This
@ -85,36 +61,8 @@
* 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}.
*
* <pre class="language language-java">{@code
* 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;
* {@snippet class=com.example.examplemod.peripheral.BrewingStandPeripheral region=body}
*
* public class FurnacePeripheral implements IPeripheral {
* 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;
* }
* }
* }</pre>
* <p>
* Finally, we'll need to register our peripheral. This is done with capabilities on Forge, or the block lookup API on
* Fabric.
*
@ -124,82 +72,11 @@
* {@code ICapabilityProvider}. If you've got an existing system for dealing with this, we recommend you use that,
* otherwise you can use something similar to the code below:
*
* <pre class="language language-java">{@code
* import dan200.computercraft.api.peripheral.IPeripheral;
* import net.minecraft.core.Direction;
* import net.minecraft.resources.ResourceLocation;
* import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
* import net.minecraft.world.level.block.entity.BlockEntity;
* import net.minecraftforge.common.capabilities.Capability;
* import net.minecraftforge.common.capabilities.CapabilityManager;
* import net.minecraftforge.common.capabilities.CapabilityToken;
* import net.minecraftforge.common.capabilities.ICapabilityProvider;
* import net.minecraftforge.common.util.LazyOptional;
* import net.minecraftforge.event.AttachCapabilitiesEvent;
* import org.jetbrains.annotations.Nullable;
*
* import java.util.function.Function;
*
* public class ComputerCraftCompat {
* public static final Capability<IPeripheral> CAPABILITY_PERIPHERAL = CapabilityManager.get(new CapabilityToken<>() {
* });
* private static final ResourceLocation PERIPHERAL = new ResourceLocation("mymod", "peripheral");
*
* public static void register(AttachCapabilitiesEvent<BlockEntity> event) {
* if (event.getObject() instanceof AbstractFurnaceBlockEntity furnace) {
* PeripheralProvider.attach(event, furnace, FurnacePeripheral::new);
* }
* }
*
* // A {@link ICapabilityProvider} that lazily creates an {@link IPeripheral} when required.
* private static class PeripheralProvider<O extends BlockEntity> implements ICapabilityProvider {
* private final O blockEntity;
* private final Function<O, IPeripheral> factory;
* private @Nullable LazyOptional<IPeripheral> peripheral;
*
* private PeripheralProvider(O blockEntity, Function<O, IPeripheral> factory) {
* this.blockEntity = blockEntity;
* this.factory = factory;
* }
*
* private static <O extends BlockEntity> void attach(AttachCapabilitiesEvent<BlockEntity> event, O blockEntity, Function<O, IPeripheral> factory) {
* var provider = new PeripheralProvider<>(blockEntity, factory);
* event.addCapability(PERIPHERAL, provider);
* event.addListener(provider::invalidate);
* }
*
* private void invalidate() {
* if (peripheral != null) peripheral.invalidate();
* peripheral = null;
* }
*
* @Override
* public <T> LazyOptional<T> getCapability(Capability<T> capability, @Nullable Direction direction) {
* if (capability != CAPABILITY_PERIPHERAL) return LazyOptional.empty();
* if (blockEntity.isRemoved()) return LazyOptional.empty();
*
* var peripheral = this.peripheral;
* return (peripheral == null ? (this.peripheral = LazyOptional.of(() -> factory.apply(blockEntity))) : peripheral).cast();
* }
* }
* }
* }</pre>
* {@snippet class=com.example.examplemod.ForgeExampleMod region=peripherals}
*
* <h4>Registering {@code IPeripheral} on Fabric</h4>
* Registering a peripheral on Fabric can be done using the block lookup API, via {@code PeripheralLookup}.
*
* <pre class="language language-java">{@code
* 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);
* }
* }
* }</pre>
* {@snippet class=com.example.examplemod.FabricExampleMod region=peripherals}
*/
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
* 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
* LOG.error("Some error occurred: {}", new TruncatedError(error));
* }</pre>

View File

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

View File

@ -106,8 +106,6 @@ dependencies {
testFixturesImplementation(testFixtures(project(":core")))
}
sourceSets.main { resources.srcDir("src/generated/resources") }
loom {
accessWidenerPath.set(project(":common").file("src/main/resources/computercraft.accesswidener"))
mixin.defaultRefmapName.set("computercraft.refmap.json")
@ -126,6 +124,10 @@ loom {
sourceSet(sourceSets.testMod.get())
sourceSet(project(":common").sourceSets.testMod.get())
}
register("examplemod") {
sourceSet(sourceSets.examples.get())
}
}
runs {
@ -142,19 +144,24 @@ loom {
runDir("run/server")
}
register("data") {
configName = "Datagen"
fun RunConfigSettings.configureForData(sourceSet: SourceSet) {
client()
source(sourceSets.datagen.get())
runDir("run/dataGen")
runDir("run/run${name.capitalise()}")
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")
}
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())
val testSources = project(":common").file("src/testMod/resources/data/cctest").absolutePath
@ -169,7 +176,7 @@ loom {
val testClient by registering {
configName = "Test Client"
client()
configureForGameTest(this)
configureForGameTest()
runDir("run/testClient")
property("cctest.tags", "client,common")
@ -178,16 +185,27 @@ loom {
register("gametest") {
configName = "Game Test"
server()
configureForGameTest(this)
configureForGameTest()
property("fabric-api.gametest")
property(
"fabric-api.gametest.report-file",
layout.buildDirectory.dir("test-results/runGametest.xml")
.getAbsolutePath(),
layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(),
)
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,30 @@
package com.example.examplemod;
import com.example.examplemod.peripheral.BrewingStandPeripheral;
import dan200.computercraft.api.peripheral.PeripheralLookup;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
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<TurtleUpgradeSerialiser<?>>) BuiltInRegistries.REGISTRY.get(TurtleUpgradeSerialiser.registryId().location());
Registry.register(turtleUpgradeSerialisers, new ResourceLocation(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,17 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleDataProvider;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.minecraft.data.DataProvider;
/**
* The data generator entrypoint for our Fabric example mod.
*/
public class FabricExampleModDataGenerator implements DataGeneratorEntrypoint {
@Override
public void onInitializeDataGenerator(FabricDataGenerator generator) {
var pack = generator.createPack();
pack.addProvider((DataProvider.Factory<?>) TurtleDataProvider::new);
}
}

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")
}
java {
withJavadocJar()
}
cct.inlineProject(":common-api")
dependencies {
@ -20,11 +16,3 @@ dependencies {
tasks.javadoc {
include("dan200/computercraft/api/**/*.java")
}
publishing {
publications {
named("maven", MavenPublication::class) {
fg.component(this)
}
}
}

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.*
import net.minecraftforge.gradle.common.util.RunConfig
import net.neoforged.moddevgradle.dsl.RunModel
plugins {
id("cc-tweaked.forge")
@ -19,105 +19,122 @@ cct {
allProjects.forEach { externalSources(it) }
}
sourceSets {
main {
resources.srcDir("src/generated/resources")
legacyForge {
val computercraft by mods.registering {
cct.sourceDirectories.get().forEach {
if (it.classes) sourceSet(it.sourceSet)
}
}
val computercraftDatagen by mods.registering {
cct.sourceDirectories.get().forEach {
if (it.classes) sourceSet(it.sourceSet)
}
sourceSet(sourceSets.datagen.get())
}
val testMod by mods.registering {
sourceSet(sourceSets.testMod.get())
sourceSet(sourceSets.testFixtures.get())
sourceSet(project(":core").sourceSets["testFixtures"])
}
val exampleMod by mods.registering {
sourceSet(sourceSets.examples.get())
}
}
minecraft {
runs {
// configureEach would be better, but we need to eagerly configure configs or otherwise the run task doesn't
// get set up properly.
all {
property("forge.logging.markers", "REGISTRIES")
property("forge.logging.console.level", "debug")
mods.register("computercraft") {
cct.sourceDirectories.get().forEach {
if (it.classes) sources(it.sourceSet)
}
}
configureEach {
ideName = "Forge - ${name.capitalise()}"
systemProperty("forge.logging.markers", "REGISTRIES")
systemProperty("forge.logging.console.level", "debug")
loadedMods.add(computercraft)
}
val client by registering {
workingDirectory(file("run"))
register("client") {
client()
}
val server by registering {
workingDirectory(file("run/server"))
arg("--nogui")
register("server") {
server()
gameDirectory = file("run/server")
programArgument("--nogui")
}
val data by registering {
workingDirectory(file("run"))
args(
"--mod", "computercraft", "--all",
"--output", layout.buildDirectory.dir("generatedResources").getAbsolutePath(),
"--existing", project(":common").file("src/main/resources/"),
"--existing", file("src/main/resources/"),
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,
)
mods.named("computercraft") {
source(sourceSets["datagen"])
}
programArgument("--mixin.config=computercraft-gametest.mixins.json")
loadedMods.add(testMod)
jvmArgument("-ea")
}
fun RunConfig.configureForGameTest() {
val old = lazyTokens["minecraft_classpath"]
lazyToken("minecraft_classpath") {
// Add all files in testMinecraftLibrary to the classpath.
val allFiles = mutableSetOf<String>()
val oldVal = old?.get()
if (!oldVal.isNullOrEmpty()) allFiles.addAll(oldVal.split(File.pathSeparatorChar))
for (file in configurations["testMinecraftLibrary"].resolve()) allFiles.add(file.absolutePath)
allFiles.joinToString(File.pathSeparator)
}
property("cctest.sources", project(":common").file("src/testMod/resources/data/cctest").absolutePath)
arg("--mixin.config=computercraft-gametest.mixins.json")
mods.register("cctest") {
source(sourceSets["testMod"])
source(sourceSets["testFixtures"])
source(project(":core").sourceSets["testFixtures"])
}
}
val testClient by registering {
workingDirectory(file("run/testClient"))
parent(client.get())
register("testClient") {
client()
gameDirectory = file("run/testClient")
configureForGameTest()
property("cctest.tags", "client,common")
systemProperty("cctest.tags", "client,common")
}
val gameTestServer by registering {
workingDirectory(file("run/testServer"))
register("gametest") {
type = "gameTestServer"
configureForGameTest()
property("forge.logging.console.level", "info")
jvmArg("-ea")
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 {
minecraftLibrary { extendsFrom(minecraftEmbed.get()) }
additionalRuntimeClasspath { extendsFrom(jarJar.get()) }
// Move minecraftLibrary/minecraftEmbed out of implementation, and into runtimeOnly.
implementation { setExtendsFrom(extendsFrom - setOf(minecraftLibrary.get(), minecraftEmbed.get())) }
runtimeOnly { extendsFrom(minecraftLibrary.get(), minecraftEmbed.get()) }
val testMinecraftLibrary by registering {
val testAdditionalRuntimeClasspath by registering {
isCanBeResolved = true
isCanBeConsumed = false
// 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()) }
}
}
@ -126,31 +143,22 @@ dependencies {
annotationProcessorEverywhere(libs.autoService)
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
libs.bundles.externalMods.forge.compile.get().map { compileOnly(fg.deobf(it)) }
libs.bundles.externalMods.forge.runtime.get().map { runtimeOnly(fg.deobf(it)) }
// fg.debof only accepts a closure to configure the dependency, so doesn't work with Kotlin. We create and configure
// the dep first, and then pass it off to ForgeGradle.
(create(variantOf(libs.create.forge) { classifier("slim") }.get()) as ExternalModuleDependency)
.apply { isTransitive = false }.let { compileOnly(fg.deobf(it)) }
modCompileOnly(libs.bundles.externalMods.forge.compile)
modRuntimeOnly(libs.bundles.externalMods.forge.runtime)
modCompileOnly(variantOf(libs.create.forge) { classifier("slim") })
// Depend on our other projects.
api(commonClasses(project(":forge-api"))) { cct.exclude(this) }
clientApi(clientClasses(project(":forge-api"))) { cct.exclude(this) }
implementation(project(":core")) { cct.exclude(this) }
minecraftEmbed(libs.cobalt) {
val version = libs.versions.cobalt.get()
jarJar.ranged(this, "[$version,${getNextVersion(version)})")
}
minecraftEmbed(libs.jzlib) {
jarJar.ranged(this, "[${libs.versions.jzlib.get()},)")
}
jarJar(libs.cobalt)
jarJar(libs.jzlib)
// We don't jar-in-jar our additional netty dependencies (see the tasks.jarJar configuration), but still want them
// on the legacy classpath.
minecraftLibrary(libs.netty.http) { isTransitive = false }
minecraftLibrary(libs.netty.socks) { isTransitive = false }
minecraftLibrary(libs.netty.proxy) { isTransitive = false }
additionalRuntimeClasspath(libs.netty.http) { isTransitive = false }
additionalRuntimeClasspath(libs.netty.socks) { isTransitive = false }
additionalRuntimeClasspath(libs.netty.proxy) { isTransitive = false }
testFixturesApi(libs.bundles.test)
testFixturesApi(libs.bundles.kotlin)
@ -163,8 +171,8 @@ dependencies {
testModImplementation(testFixtures(project(":forge")))
// Ensure our test fixture dependencies are on the classpath
"testMinecraftLibrary"(libs.bundles.kotlin)
"testMinecraftLibrary"(libs.bundles.test)
"testAdditionalRuntimeClasspath"(libs.bundles.kotlin)
"testAdditionalRuntimeClasspath"(libs.bundles.test)
testFixturesImplementation(testFixtures(project(":core")))
}
@ -181,23 +189,6 @@ tasks.processResources {
}
tasks.jar {
finalizedBy("reobfJar")
archiveClassifier.set("slim")
for (source in cct.sourceDirectories.get()) {
if (source.classes && source.external) from(source.sourceSet.output)
}
}
tasks.sourcesJar {
for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource)
}
tasks.jarJar {
finalizedBy("reobfJarJar")
archiveClassifier.set("")
duplicatesStrategy = DuplicatesStrategy.FAIL
// Include all classes from other projects except core.
val coreSources = project(":core").sourceSets["main"]
for (source in cct.sourceDirectories.get()) {
@ -210,7 +201,9 @@ tasks.jarJar {
}
}
tasks.assemble { dependsOn("jarJar") }
tasks.sourcesJar {
for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource)
}
// Check tasks
@ -218,22 +211,15 @@ tasks.test {
systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath())
}
val runGametest by tasks.registering(JavaExec::class) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Runs tests on a temporary Minecraft instance."
dependsOn("cleanRunGametest")
val runGametest = tasks.named<JavaExec>("runGametest") {
usesService(MinecraftRunnerService.get(gradle))
setRunConfig(minecraft.runs["gameTestServer"])
systemProperty("cctest.gametest-report", layout.buildDirectory.dir("test-results/$name.xml").getAbsolutePath())
}
cct.jacoco(runGametest)
tasks.check { dependsOn(runGametest) }
val runGametestClient by tasks.registering(ClientJavaExec::class) {
description = "Runs client-side gametests with no mods"
setRunConfig(minecraft.runs["testClient"])
copyFromForge("runTestClient")
tags("client")
}
cct.jacoco(runGametestClient)
@ -247,22 +233,12 @@ tasks.register("checkClient") {
// Upload tasks
modPublishing {
output.set(tasks.jarJar)
}
// Don't publish the slim jar
for (cfg in listOf(configurations.apiElements, configurations.runtimeElements)) {
cfg.configure { artifacts.removeIf { it.classifier == "slim" } }
output.set(tasks.reobfJar)
}
publishing {
publications {
named("maven", MavenPublication::class) {
fg.component(this)
// jarJar.component is broken (https://github.com/MinecraftForge/ForgeGradle/issues/914), so declare the
// artifact explicitly.
artifact(tasks.jarJar)
mavenDependencies {
cct.configureExcludes(this)
exclude(libs.jei.forge.get())

View File

@ -0,0 +1,92 @@
package com.example.examplemod;
import com.example.examplemod.peripheral.BrewingStandPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityToken;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.RegisterEvent;
import javax.annotation.Nullable;
import java.util.function.Function;
/**
* The main entry point for the Forge version of our example mod.
*/
@Mod(ExampleMod.MOD_ID)
public class ForgeExampleMod {
public ForgeExampleMod() {
// 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
var modBus = FMLJavaModLoadingContext.get().getModEventBus();
modBus.addListener((RegisterEvent event) -> {
event.register(TurtleUpgradeSerialiser.registryId(), new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"), () -> ExampleMod.EXAMPLE_TURTLE_UPGRADE);
});
// @end region=turtle_upgrades
modBus.addListener((FMLCommonSetupEvent event) -> ExampleMod.registerComputerCraft());
MinecraftForge.EVENT_BUS.addGenericListener(BlockEntity.class, ForgeExampleMod::attachPeripherals);
}
// @start region=peripherals
// The main function to attach peripherals to block entities. This should be added to the Forge event bus.
public static void attachPeripherals(AttachCapabilitiesEvent<BlockEntity> event) {
if (event.getObject() instanceof BrewingStandBlockEntity brewingStand) {
PeripheralProvider.attach(event, brewingStand, BrewingStandPeripheral::new);
}
}
// Boilerplate for adding a new capability provider
public static final Capability<IPeripheral> CAPABILITY_PERIPHERAL = CapabilityManager.get(new CapabilityToken<>() {
});
private static final ResourceLocation PERIPHERAL = new ResourceLocation(ExampleMod.MOD_ID, "peripheral");
// A {@link ICapabilityProvider} that lazily creates an {@link IPeripheral} when required.
private static final class PeripheralProvider<O extends BlockEntity> implements ICapabilityProvider {
private final O blockEntity;
private final Function<O, IPeripheral> factory;
private @Nullable LazyOptional<IPeripheral> peripheral;
private PeripheralProvider(O blockEntity, Function<O, IPeripheral> factory) {
this.blockEntity = blockEntity;
this.factory = factory;
}
private static <O extends BlockEntity> void attach(AttachCapabilitiesEvent<BlockEntity> event, O blockEntity, Function<O, IPeripheral> factory) {
var provider = new PeripheralProvider<>(blockEntity, factory);
event.addCapability(PERIPHERAL, provider);
event.addListener(provider::invalidate);
}
private void invalidate() {
if (peripheral != null) peripheral.invalidate();
peripheral = null;
}
@Override
public <T> LazyOptional<T> getCapability(Capability<T> capability, @Nullable Direction direction) {
if (capability != CAPABILITY_PERIPHERAL) return LazyOptional.empty();
if (blockEntity.isRemoved()) return LazyOptional.empty();
var peripheral = this.peripheral;
return (peripheral == null ? (this.peripheral = LazyOptional.of(() -> factory.apply(blockEntity))) : peripheral).cast();
}
}
// @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.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* The client-side entry point for the Forge version of our example mod.
*/
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT, bus = Mod.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,19 @@
package com.example.examplemod;
import com.example.examplemod.data.ExampleModDataGenerators;
import net.minecraftforge.data.event.GatherDataEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
/**
* The data generator entrypoint for the Forge version of our example mod.
*
* @see ExampleModDataGenerators The main implementation
*/
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class ForgeExampleModDataGenerator {
@SubscribeEvent
public static void gather(GatherDataEvent event) {
ExampleModDataGenerators.run(event.getGenerator().getVanillaPack(true));
}
}

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

@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.javadoc
import com.sun.source.doctree.DocTree
import com.sun.source.doctree.TextTree
import com.sun.source.doctree.UnknownInlineTagTree
import com.sun.source.util.DocTreePath
import jdk.javadoc.doclet.*
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.regex.Pattern
import javax.lang.model.element.Element
import javax.tools.Diagnostic
import kotlin.io.path.extension
/**
* A primitive reimplementation of Java 21's `@snippet` tag. This only supports including external snippets via `file`
* and `class`, and not the inline body.
*/
class SnippetTaglet : Taglet {
override fun getName(): String = "snippet"
override fun isInlineTag(): Boolean = true
override fun getAllowedLocations(): Set<Taglet.Location> = locations
private lateinit var env: DocletEnvironment
private lateinit var reporter: Reporter
private lateinit var snippetPath: List<File>
override fun init(env: DocletEnvironment, doclet: Doclet) {
super.init(env, doclet)
this.env = env
reporter = (doclet as StandardDoclet).reporter
this.snippetPath =
System.getProperty("cc.snippet-path")?.split(File.pathSeparatorChar)?.map { File(it) } ?: emptyList()
}
/** Parse our attributes into a key/value map */
private fun parseAttributes(contents: String): Map<String, String> {
val attributes = mutableMapOf<String, String>()
val attributeMatcher = attribute.matcher(contents)
var lastIndex = 0
while (attributeMatcher.find()) {
val key = attributeMatcher.group(1)
val value = attributeMatcher.group(2)
if (attributes.contains(key)) throw SnippetException("Duplicate attribute '$key'")
attributes[key] = value
lastIndex = attributeMatcher.end()
}
while (lastIndex < contents.length) {
val c = contents[lastIndex]
if (c != ' ') throw SnippetException("Unexpected '$c'")
}
return attributes
}
/** Locate our snippet file within the [snippetPath] */
private fun findSnippetFile(fileName: String): Path = snippetPath.firstNotNullOfOrNull {
val found = it.resolve(fileName)
if (found.exists()) found.toPath() else null
} ?: throw SnippetException("Cannot find file '$fileName'")
private fun processInlineTag(tag: UnknownInlineTagTree): String {
val tagContent = tag.content
if (tagContent.size != 1 || tagContent[0].kind != DocTree.Kind.TEXT) throw SnippetException("Expected a single text node")
val attributes = parseAttributes((tagContent[0] as TextTree).body)
val hasFile = attributes.contains("file")
val hasClass = attributes.contains("class")
if (hasFile && hasClass) throw SnippetException("Cannot specify file and class")
val file = when {
hasFile -> findSnippetFile(attributes["file"]!!)
hasClass -> findSnippetFile(attributes["class"]!!.replace('.', '/') + ".java")
else -> throw SnippetException("Snippet has no contents (must have file or class)")
}
// And generate our snippet
var snippetContents = Files.readString(file)
val region = attributes["region"]
if (region != null) {
val matcher =
Pattern.compile("// @start region=" + Pattern.quote(region) + "\n(.*)\\s*// @end region=" + Pattern.quote(region), Pattern.DOTALL)
.matcher(snippetContents)
if (!matcher.find()) throw SnippetException("Cannot find region '$region'")
snippetContents = matcher.group(1).trimIndent()
}
return makeSnippet(file.extension, snippetContents)
}
override fun toString(tags: List<DocTree>, element: Element): String {
if (tags.size != 1) throw IllegalArgumentException("Tags should be length 1")
val tag = tags[0] as UnknownInlineTagTree
try {
return processInlineTag(tag)
} catch (e: SnippetException) {
reporter.print(
Diagnostic.Kind.ERROR,
DocTreePath.getPath(env.docTrees.getPath(element), env.docTrees.getDocCommentTree(element), tag),
"Invalid @snippet. ${e.message}",
)
return "@snippet"
}
}
companion object {
private val locations = EnumSet.allOf(Taglet.Location::class.java)
private val attribute = Pattern.compile(" *([a-z]+) *= *([^ ]+)")
/** Escape our snippet HTML and wrap it into a code block */
private fun makeSnippet(extension: String, contents: String): String {
val out = StringBuilder(contents.length + 60)
out.append("<pre class=\"language language-$extension\"><code>")
for (element in contents) {
when (element) {
'<' -> out.append("&lt;")
'>' -> out.append("&gt;")
'&' -> out.append("&amp;")
else -> out.append(element)
}
}
out.append("</code></pre>")
return out.toString()
}
}
}
private class SnippetException(message: String) : Exception(message)

View File

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

View File

@ -8,25 +8,10 @@ pluginManagement {
mavenCentral()
gradlePluginPortal()
maven("https://maven.minecraftforge.net") {
name = "Forge"
maven("https://maven.neoforged.net") {
name = "NeoForge"
content {
includeGroup("net.minecraftforge")
includeGroup("net.minecraftforge.gradle")
}
}
maven("https://maven.parchmentmc.org") {
name = "Librarian"
content {
includeGroupByRegex("^org\\.parchmentmc.*")
}
}
maven("https://repo.spongepowered.org/repository/maven-public/") {
name = "Sponge"
content {
includeGroup("org.spongepowered")
includeGroup("net.neoforged")
}
}
@ -45,14 +30,6 @@ pluginManagement {
}
}
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.spongepowered.mixin") {
useModule("org.spongepowered:mixingradle:${requested.version}")
}
}
}
}
val mcVersion: String by settings