diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb23a776f..57a8eb11a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 ) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38729d846..6903da8fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. ## Translations -Translations are managed through [CrowdIn], an online interface for managing language strings. Translations may either -be contributed there, or directly via a pull request. +Translations are managed through [CrowdIn], an online interface for managing language strings. ## Setting up a development environment 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). ## Developing CC: Tweaked -Before making any major changes to CC: Tweaked, I'd recommend you 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]! +Before making any major changes to CC: Tweaked, I'd recommend starting opening an issue or starting a discussion on +GitHub first. It's often helpful to discuss features before spending time developing them! + +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 When making larger changes, it may be useful to write a test to make sure your code works as expected. diff --git a/README.md b/README.md index 674b7ad98..2445980e7 100644 --- a/README.md +++ b/README.md @@ -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 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! diff --git a/REUSE.toml b/REUSE.toml index 273ab494d..124152c6f 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -8,10 +8,10 @@ SPDX-PackageSupplier = "Jonathan Coates " 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]] @@ -74,7 +79,7 @@ path = [ ] [[annotations]] -# Community-contributed license files +# Community-contributed language files SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" SPDX-License-Identifier = "LicenseRef-CCPL" path = [ @@ -88,18 +93,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 " -SPDX-License-Identifier = "CC0-1.0" -path = ".github/**" - [[annotations]] path = ["gradle/wrapper/**"] SPDX-FileCopyrightText = "Gradle Inc" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index f8712e197..e03247564 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -14,14 +14,10 @@ repositories { mavenCentral() gradlePluginPortal() - maven("https://maven.neoforged.net/releases") { + maven("https://maven.neoforged.net") { name = "NeoForge" content { - includeGroup("net.minecraftforge") 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.ideaExt) implementation(libs.minotaur) - implementation(libs.neoGradle.userdev) + implementation(libs.modDevGradle) implementation(libs.vanillaExtract) } diff --git a/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts index faeb4ba1f..54155d2e6 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts @@ -11,20 +11,18 @@ import cc.tweaked.gradle.MinecraftConfigurations plugins { id("cc-tweaked.java-convention") - id("net.neoforged.gradle.userdev") + id("net.neoforged.moddev") } plugins.apply(CCTweakedPlugin::class.java) val mcVersion: String by extra -minecraft { - modIdentifier("computercraft") -} +neoForge { + val libs = project.extensions.getByType().named("libs") + version = libs.findVersion("neoForge").get().toString() -subsystems { parchment { - val libs = project.extensions.getByType().named("libs") minecraftVersion = libs.findVersion("parchmentMc").get().toString() mappingsVersion = libs.findVersion("parchment").get().toString() } diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index b5a1858d3..1550933d8 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -99,6 +99,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( diff --git a/buildSrc/src/main/kotlin/cc-tweaked.mod.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.mod.gradle.kts index fb12f8120..07b1427dc 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.mod.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.mod.gradle.kts @@ -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. diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedExtension.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedExtension.kt index 0124fee5c..44d8dfacc 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedExtension.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedExtension.kt @@ -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) } } diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftConfigurations.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftConfigurations.kt index dce07bf96..6dbe9fbd3 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftConfigurations.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftConfigurations.kt @@ -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) + } } } diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt index 20baaccfc..b143cb2bf 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt @@ -4,6 +4,7 @@ package cc.tweaked.gradle +import net.neoforged.moddevgradle.internal.RunGameTask import org.gradle.api.GradleException import org.gradle.api.file.FileSystemOperations import org.gradle.api.invocation.Gradle @@ -65,6 +66,22 @@ abstract class ClientJavaExec : JavaExec() { 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. */ diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 9347a3622..0a3efaa20 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -133,7 +133,7 @@ SPDX-License-Identifier: MPL-2.0 - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b6569113..914e2d42c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ ideaExt = "1.1.7" illuaminate = "0.1.0-74-gf1551d5" lwjgl = "3.3.3" minotaur = "2.8.7" -neoGradle = "7.0.170" +modDevGradle = "2.0.74" nullAway = "0.10.25" shadow = "8.3.1" 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" } kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 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" } +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" } diff --git a/projects/common-api/build.gradle.kts b/projects/common-api/build.gradle.kts index 0eeb0a534..b5649f8c1 100644 --- a/projects/common-api/build.gradle.kts +++ b/projects/common-api/build.gradle.kts @@ -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,12 @@ tasks.javadoc { """.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. diff --git a/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java b/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java index 4adfc8d91..8dfb5c173 100644 --- a/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java +++ b/projects/common-api/src/client/java/dan200/computercraft/api/client/turtle/TurtleUpgradeModeller.java @@ -21,7 +21,14 @@ import java.util.stream.Stream; *

* 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. + * + *

Example

+ *

Fabric

+ * {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers} + * + *

Forge

+ * {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers} * * @param The type of turtle upgrade this modeller applies to. * @see RegisterTurtleUpgradeModeller For multi-loader registration support. diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java b/projects/common-api/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java index 604cbaa95..1a75ae954 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java @@ -171,16 +171,9 @@ public final class ComputerCraftAPI { * using {@link ILuaAPI#getModuleName()} to expose this library as a module instead of as a global. *

* 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": - * 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); - * }); - * } + * {@snippet class=com.example.examplemod.ExampleAPI region=register} * * @param factory The factory for your API subclass. * @see ILuaAPIFactory diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java index 1b3c9c42f..2821590c8 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java @@ -20,32 +20,10 @@ import javax.annotation.Nullable; * A peripheral which can be equipped to the back side of a pocket computer. *

* 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. *

* 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 - * data generators. - * - *

Example

- * {@snippet lang="java" : - * // We use Forge's DeferredRegister to register our upgrade type. Fabric mods may register their type directly. - * static final DeferredRegister> POCKET_UPGRADES = DeferredRegister.create(IPocketUpgrade.typeRegistry(), "my_mod"); - * - * // Register a new upgrade upgrade type called "my_upgrade". - * public static final RegistryObject> MY_UPGRADE = - * POCKET_UPGRADES.register("my_upgrade", () -> UpgradeType.simple(new MyUpgrade())); - * - * // Then in your constructor - * POCKET_UPGRADES.register(bus); - * } - *

- * We can then define a new upgrade using JSON by placing the following in - * {@code data//computercraft/pocket_upgrade/.json}. - * {@snippet lang="json" : - * { - * "type": "my_mod:my_upgrade" - * } - * } + * the upgrade registered internally. */ public interface IPocketUpgrade extends UpgradeBase { ResourceKey> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "pocket_upgrade")); diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java index 4241d501a..babc5586b 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java @@ -11,47 +11,79 @@ import dan200.computercraft.api.upgrades.UpgradeType; import dan200.computercraft.impl.ComputerCraftAPIService; import net.minecraft.core.Direction; import net.minecraft.core.Registry; +import net.minecraft.core.RegistrySetBuilder.PatchedRegistries; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Items; 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 * peripheral. *

* 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. *

* 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 - * data generators. + * the upgrade automatically registered. * *

Example

- * {@snippet lang="java" : - * // We use Forge's DeferredRegister to register our upgrade type. Fabric mods may register their type directly. - * static final DeferredRegister> TURTLE_UPGRADES = DeferredRegister.create(ITurtleUpgrade.typeRegistry(), "my_mod"); + *

Registering the upgrade type

+ * 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. * - * // Register a new upgrade type called "my_upgrade". - * public static final RegistryObject> MY_UPGRADE = - * TURTLE_UPGRADES.register("my_upgrade", () -> UpgradeType.simple(MyUpgrade::new)); + * {@snippet class=com.example.examplemod.ExampleTurtleUpgrade region=body} * - * // Then in your constructor - * TURTLE_UPGRADES.register(bus); - * } + * Now we must construct a new upgrade type. In most cases, you can use one of the helper methods (e.g. + * {@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. + * + *

Fabric

+ * {@snippet class=com.example.examplemod.FabricExampleMod region=turtle_upgrades} + * + *

Forge

+ * {@snippet class=com.example.examplemod.ForgeExampleMod region=turtle_upgrades} + * + *

Rendering the upgrade

+ * 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. + * + *

Fabric

+ * {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers} + * + *

Forge

+ * {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_modellers} + * + *

Registering the upgrade itself

+ * 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//computercraft/turtle_upgrade/.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}. *

- * We can then define a new upgrade using JSON by placing the following in - * {@code data//computercraft/turtle_upgrade/.json}. - *

- * {@snippet lang="json" : - * { - * "type": "my_mod:my_upgrade" - * } - * } - *

- * Finally, we need to register a model for our upgrade, see - * {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information. + * Rather than manually creating the file, it is recommended to use data-generators to generate this file. First, we + * register our new upgrades into a {@linkplain PatchedRegistries patched registry}. + * + * {@snippet class=com.example.examplemod.data.TurtleUpgradeProvider region=body} + * + * 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. + * + *

Fabric

+ * {@snippet class=com.example.examplemod.FabricExampleModDataGenerator region=turtle_upgrades} + * + *

Forge

+ * {@snippet class=com.example.examplemod.ForgeExampleModDataGenerator region=turtle_upgrades} */ public interface ITurtleUpgrade extends UpgradeBase { /** diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolBuilder.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolBuilder.java index 485dad265..d1b0cb1c0 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolBuilder.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleToolBuilder.java @@ -25,19 +25,11 @@ import java.util.Optional; /** * A builder for custom turtle tool upgrades. *

- * This can be used from your data generator code in order to + * This can be used from your data generator code in order to * register turtle tools for your mod's tools. * - *

Example:

- * {@snippet lang = "java": - * import net.minecraft.data.worldgen.BootstrapContext; - * import net.minecraft.resources.ResourceLocation; - * import net.minecraft.world.item.Items; - * - * public void registerTool(BootstrapContext upgrades) { - * TurtleToolBuilder.tool(ResourceLocation.fromNamespaceAndPath("my_mod", "wooden_pickaxe"), Items.WOODEN_PICKAXE).register(upgrades); - * } - *} + *

Example

+ * {@snippet class=com.example.examplemod.data.TurtleToolProvider region=body} */ public final class TurtleToolBuilder { private final ResourceKey id; diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeType.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeType.java index 268ae5e2a..19baddc2d 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeType.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeType.java @@ -7,9 +7,7 @@ package dan200.computercraft.api.upgrades; import com.mojang.serialization.MapCodec; import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.api.turtle.ITurtleUpgrade; -import dan200.computercraft.impl.upgrades.UpgradeTypeImpl; import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.data.registries.RegistryPatchGenerator; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; @@ -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 * loot functions}. *

- * First, one adds a new class implementing {@link ITurtleUpgrade} or {@link IPocketUpgrade}). This is responsible for - * handling all the logic of your upgrade. - *

- * However, the upgrades are not registered directly. Instead, each upgrade class should have a corresponding - * {@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()}). + * While the {@link ITurtleUpgrade}/{@link IPocketUpgrade} class should contain the core logic of the upgrade, they are + * not registered directly. Instead, each upgrade class has a corresponding {@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()}). *

* 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. @@ -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 * tools as you would for any other dynamic registry. *

- * This can typically be done by extending vanilla's built-in registries using {@link RegistryPatchGenerator}, and then - * writing out the new registries using {@code net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider} - * on Fabric or {@code net.neoforged.neoforge.common.data.DatapackBuiltinEntriesProvider} on Forge. - *

- * {@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 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"))); - * } - * } + * See the turtle upgrade docs for a concrete example. * * @param The upgrade subclass that this upgrade type represents. * @see ITurtleUpgrade diff --git a/projects/common-api/src/main/java/dan200/computercraft/impl/upgrades/UpgradeTypeImpl.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeTypeImpl.java similarity index 51% rename from projects/common-api/src/main/java/dan200/computercraft/impl/upgrades/UpgradeTypeImpl.java rename to projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeTypeImpl.java index 4c3dcd91a..940862bdd 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/impl/upgrades/UpgradeTypeImpl.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeTypeImpl.java @@ -2,12 +2,9 @@ // // SPDX-License-Identifier: MPL-2.0 -package dan200.computercraft.impl.upgrades; +package dan200.computercraft.api.upgrades; 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}. @@ -15,6 +12,5 @@ import org.jetbrains.annotations.ApiStatus; * @param codec The codec to read/write upgrades with. * @param The upgrade subclass that this upgrade type represents. */ -@ApiStatus.Internal -public record UpgradeTypeImpl(MapCodec codec) implements UpgradeType { +record UpgradeTypeImpl(MapCodec codec) implements UpgradeType { } diff --git a/projects/common-api/src/overview.html b/projects/common-api/src/overview.html new file mode 100644 index 000000000..f55e97826 --- /dev/null +++ b/projects/common-api/src/overview.html @@ -0,0 +1,68 @@ + + + + + +

+ This is the documentation for CC: Tweaked $modVersion for Minecraft $mcVersion. Documentation for other versions of + Minecraft are available on the CC: Tweaked website: + +

+ +

Quick links

+

+ You probably want to start in the following places: + +

    +
  • {@linkplain dan200.computercraft.api.peripheral Registering new peripherals}
  • +
  • + {@link dan200.computercraft.api.lua.LuaFunction} and {@link dan200.computercraft.api.lua.IArguments} for + adding methods to your peripheral or Lua objects. +
  • +
  • {@linkplain dan200.computercraft.api.turtle.ITurtleUpgrade Turtle upgrades}
  • +
  • {@linkplain dan200.computercraft.api.pocket.IPocketUpgrade Pocket upgrades}
  • +
+ +

Using

+

+ 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 mods.toml file, with the appropriate version bounds, to ensure that API + functionality you depend on is present. + +

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")
+}
+
+ +

+ 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 start a discussion to + let me know! + + + diff --git a/projects/common/build.gradle.kts b/projects/common/build.gradle.kts index a90db51a2..f3699db99 100644 --- a/projects/common/build.gradle.kts +++ b/projects/common/build.gradle.kts @@ -11,12 +11,6 @@ plugins { id("cc-tweaked.publishing") } -sourceSets { - main { - resources.srcDir("src/generated/resources") - } -} - minecraft { accessWideners( "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::") } } -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 } diff --git a/projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json b/projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json new file mode 100644 index 000000000..5aa5c5b76 --- /dev/null +++ b/projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json @@ -0,0 +1,4 @@ +{ + "type": "examplemod:example_turtle_upgrade", + "item": "minecraft:compass" +} \ No newline at end of file diff --git a/projects/common/src/examples/java/com/example/examplemod/ExampleAPI.java b/projects/common/src/examples/java/com/example/examplemod/ExampleAPI.java new file mode 100644 index 000000000..d75ab11b2 --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/ExampleAPI.java @@ -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. + *

+ * This API is not available as a global (as {@link #getNames() returns nothing}), but is instead accessible via + * {@code require} (see {@link #getModuleName()}). + * + *

Example

+ *
{@code
+ * local my_api = require("example.my_api")
+ * print("Turtle is facing " .. my_api.getDirection())
+ * }
+ */ +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 myString) { + String contents = myString.value(); + System.out.println("Got " + contents); + } + // @end region=coerced +} diff --git a/projects/common/src/examples/java/com/example/examplemod/ExampleMod.java b/projects/common/src/examples/java/com/example/examplemod/ExampleMod.java new file mode 100644 index 000000000..8a54e743a --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/ExampleMod.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * This only defines the upgrade type. See {@link TurtleUpgradeProvider} for defining the actual upgrade. + */ + // @start region=turtle_upgrades + public static final UpgradeType 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(); + } +} diff --git a/projects/common/src/examples/java/com/example/examplemod/ExampleTurtleUpgrade.java b/projects/common/src/examples/java/com/example/examplemod/ExampleTurtleUpgrade.java new file mode 100644 index 000000000..895b173a4 --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/ExampleTurtleUpgrade.java @@ -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 getType() { + return ExampleMod.EXAMPLE_TURTLE_UPGRADE; + } +} +// @end region=body diff --git a/projects/common/src/examples/java/com/example/examplemod/data/TurtleToolProvider.java b/projects/common/src/examples/java/com/example/examplemod/data/TurtleToolProvider.java new file mode 100644 index 000000000..c1a21adbd --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/data/TurtleToolProvider.java @@ -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. + *

+ * 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 upgrades) { + TurtleToolBuilder + .tool(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "wooden_pickaxe"), Items.WOODEN_PICKAXE) + .register(upgrades); + } + // @end region=body +} diff --git a/projects/common/src/examples/java/com/example/examplemod/data/TurtleUpgradeProvider.java b/projects/common/src/examples/java/com/example/examplemod/data/TurtleUpgradeProvider.java new file mode 100644 index 000000000..156330962 --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/data/TurtleUpgradeProvider.java @@ -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 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 makeUpgradeRegistry(CompletableFuture registries) { + return RegistryPatchGenerator.createLookup(registries, Util.make(new RegistrySetBuilder(), builder -> { + builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades); + })); + } +} +// @end region=body diff --git a/projects/common/src/examples/java/com/example/examplemod/package-info.java b/projects/common/src/examples/java/com/example/examplemod/package-info.java new file mode 100644 index 000000000..bca9fdaea --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/package-info.java @@ -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; diff --git a/projects/common/src/examples/java/com/example/examplemod/peripheral/BrewingStandPeripheral.java b/projects/common/src/examples/java/com/example/examplemod/peripheral/BrewingStandPeripheral.java new file mode 100644 index 000000000..0e179e49a --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/peripheral/BrewingStandPeripheral.java @@ -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 diff --git a/projects/common/src/examples/java/com/example/examplemod/peripheral/ComputerTrackingPeripheral.java b/projects/common/src/examples/java/com/example/examplemod/peripheral/ComputerTrackingPeripheral.java new file mode 100644 index 000000000..3933d5147 --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/peripheral/ComputerTrackingPeripheral.java @@ -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 diff --git a/projects/common/src/examples/java/com/example/examplemod/peripheral/FurnacePeripheral.java b/projects/common/src/examples/java/com/example/examplemod/peripheral/FurnacePeripheral.java new file mode 100644 index 000000000..e09ba7f1c --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/peripheral/FurnacePeripheral.java @@ -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 diff --git a/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index 5eab546fa..029f681bb 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -747,6 +747,7 @@ public class TurtleAPI implements ILuaAPI { * @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.since 1.64 + * @cc.changed 1.90.0 Added detailed parameter. * @cc.usage Print the current slot, assuming it contains 13 dirt. * *

{@code
diff --git a/projects/core-api/build.gradle.kts b/projects/core-api/build.gradle.kts
index 4341fba36..2005b8695 100644
--- a/projects/core-api/build.gradle.kts
+++ b/projects/core-api/build.gradle.kts
@@ -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
diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/Coerced.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/Coerced.java
index 0d981641c..18fc99d9c 100644
--- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/Coerced.java
+++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/Coerced.java
@@ -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.
  *
- * 

Example:

- * {@snippet lang="java" : - * @LuaFunction - * public final void doSomething(Coerced myString) { - * var value = myString.value(); - * } - * } + *

Example

+ * {@snippet class=com.example.examplemod.ExampleAPI region=coerced} * * @param value The argument value. * @param The type of the underlying value. diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/lua/GenericSource.java b/projects/core-api/src/main/java/dan200/computercraft/api/lua/GenericSource.java index 5ea1fa811..a8c170f55 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/lua/GenericSource.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/lua/GenericSource.java @@ -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()}. - *

- * For example, the main CC: Tweaked mod defines a generic source for inventories, which works on {@code IItemHandler}s: * - * {@snippet lang="java" : - * public class InventoryMethods implements GenericSource { - * @LuaFunction(mainThread = true) - * public int size(IItemHandler inventory) { - * return inventory.getSlots(); - * } - * - * // ... - * } - * } + *

Example

+ * {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body} *

- * 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) */ diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/package-info.java b/projects/core-api/src/main/java/dan200/computercraft/api/package-info.java index 9d15df9be..c200a481b 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/package-info.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/package-info.java @@ -4,17 +4,6 @@ /** * ComputerCraft's public API. - *

- * You probably want to start in the following places: - *

    - *
  • {@link dan200.computercraft.api.peripheral} for registering new peripherals.
  • - *
  • - * {@link dan200.computercraft.api.lua.LuaFunction} and {@link dan200.computercraft.api.lua.IArguments} for - * adding methods to your peripheral or Lua objects. - *
  • - *
  • {@link dan200.computercraft.api.turtle.ITurtleUpgrade} for turtle upgrades.
  • - *
  • {@link dan200.computercraft.api.pocket.IPocketUpgrade} for pocket upgrades.
  • - *
*/ @DefaultQualifier(value = NonNull.class, locations = { TypeUseLocation.RETURN, diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/AttachedComputerSet.java b/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/AttachedComputerSet.java index e55061f27..33fb31eb2 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/AttachedComputerSet.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/AttachedComputerSet.java @@ -27,21 +27,7 @@ import java.util.function.Consumer; * *

Example

* - * {@snippet lang="java" : - * 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); - * } - * } - * } + * {@snippet class=com.example.examplemod.peripheral.ComputerTrackingPeripheral region=body} * * @see IComputerAccess * @see IPeripheral#attach(IComputerAccess) diff --git a/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/package-info.java b/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/package-info.java index 1ab029773..53b295a69 100644 --- a/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/package-info.java +++ b/projects/core-api/src/main/java/dan200/computercraft/api/peripheral/package-info.java @@ -47,36 +47,12 @@ * 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 * {@link dan200.computercraft.api.lua.LuaFunction} to expose it to computers. - *

- * {@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 { - * @Override - * public String id() { - * return "mymod:furnace"; - * } + * {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body} * - * @LuaFunction(mainThread = true) - * public int getBurnTime(AbstractFurnaceBlockEntity furnace) { - * return furnace.litTime; - * } - * } - * } - *

* Finally, we need to register our peripheral, so that ComputerCraft is aware of it: - *

- * {@snippet lang="java" : - * import dan200.computercraft.api.ComputerCraftAPI; * - * public class ComputerCraftCompat { - * public static void register() { - * ComputerCraftAPI.registerGenericSource(new FurnacePeripheral()); - * } - * } - * } + * {@snippet class=com.example.examplemod.ExampleMod region=generic_source} * *

Creating a {@code IPeripheral}

* First, we'll need to create a new class that implements {@link dan200.computercraft.api.peripheral.IPeripheral}. This @@ -84,72 +60,20 @@ *

* 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}. - *

- * {@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 { - * private final AbstractFurnaceBlockEntity furnace; + * {@snippet class=com.example.examplemod.peripheral.BrewingStandPeripheral region=body} * - * 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; - * } - * } - * } - *

* Finally, we'll need to register our peripheral. This is done with capabilities on Forge, or the block lookup API on * Fabric. * *

Registering {@code IPeripheral} on Forge

* Registering a peripheral on Forge can be done by using the capability API, via {@code PeripheralCapability}. * - * {@snippet lang="java" : - * 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)); - * } - * } - * } + * {@snippet class=com.example.examplemod.ForgeExampleMod region=peripherals} * *

Registering {@code IPeripheral} on Fabric

* Registering a peripheral on Fabric can be done using the block lookup API, via {@code PeripheralLookup}. * - * {@snippet lang="java" : - * 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); - * } - * } - * } + * {@snippet class=com.example.examplemod.FabricExampleMod region=peripherals} */ package dan200.computercraft.api.peripheral; diff --git a/projects/core/src/main/java/dan200/computercraft/core/util/SanitisedError.java b/projects/core/src/main/java/dan200/computercraft/core/util/SanitisedError.java index 2bf571d28..b1f4beb5d 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/util/SanitisedError.java +++ b/projects/core/src/main/java/dan200/computercraft/core/util/SanitisedError.java @@ -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. * - *

Example:

+ *

Example

*
{@code
  * LOG.error("Some error occurred: {}", new TruncatedError(error));
  * }
diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/fs.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/fs.lua index a18619234..1931b2842 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/fs.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/fs.lua @@ -223,16 +223,21 @@ end --- Returns true if a path is mounted to the parent filesystem. -- -- 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 --- make other mount types by correctly assigning their return value for getDrive. +-- the rom folder. -- -- @tparam string path The path to check. -- @treturn boolean If the path is mounted, rather than a normal file/folder. -- @throws If the path does not exist. -- @see getDrive -- @since 1.87.0 -function fs.isDriveRoot(sPath) - expect(1, sPath, "string") +function fs.isDriveRoot(path) + expect(1, path, "string") + + local parent = fs.getDir(path) + -- 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 diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua index 6291318da..24795a71b 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua @@ -147,7 +147,7 @@ handleMetatable = { if format == "l" then 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 elseif format == "a" then if handle.readAll then res = handle.readAll() or "" end diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua index 44de78b78..917ca9036 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua @@ -35,7 +35,7 @@ local function getFilename(sUrl) return sUrl:match("/([^/]+)$") end -local function get(sUrl) +local function get(url) -- Check if the URL is valid local ok, err = http.checkURL(url) if not ok then @@ -43,12 +43,12 @@ local function get(sUrl) return end - write("Connecting to " .. sUrl .. "... ") + write("Connecting to " .. url .. "... ") - local response = http.get(sUrl) + local response, err = http.get(url) if not response then - print("Failed.") - return nil + printError(err) + return end print("Success.") diff --git a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua index fd55dfe44..1ad30be41 100644 --- a/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua +++ b/projects/core/src/main/resources/data/computercraft/lua/rom/programs/lua.lua @@ -104,7 +104,7 @@ while running do end else printError(results[2]) - require "cc.internal.exception".report(results[2], results[3], chunk_map) + exception.report(results[2], results[3], chunk_map) end else local parser = require "cc.internal.syntax" diff --git a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua index 084511173..05fc9c849 100644 --- a/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/projects/core/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -83,6 +83,10 @@ describe("The fs library", function() expect(fs.isDriveRoot("/rom/startup.lua")):eq(false) expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false) end) + + it("returns false for missing files", function() + expect(fs.isDriveRoot("does_not_exist")):eq(false) + end) end) describe("fs.list", function() @@ -555,6 +559,22 @@ describe("The fs library", function() 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() it("errors on non-existent files", function() expect.error(fs.attributes, "xuxu_nao_existe"):eq("/xuxu_nao_existe: No such file") diff --git a/projects/fabric-api/build.gradle.kts b/projects/fabric-api/build.gradle.kts index 4ec3ab678..4e60bae75 100644 --- a/projects/fabric-api/build.gradle.kts +++ b/projects/fabric-api/build.gradle.kts @@ -7,10 +7,6 @@ plugins { id("cc-tweaked.publishing") } -java { - withJavadocJar() -} - cct.inlineProject(":common-api") dependencies { diff --git a/projects/fabric/build.gradle.kts b/projects/fabric/build.gradle.kts index 2fd654fb4..017931165 100644 --- a/projects/fabric/build.gradle.kts +++ b/projects/fabric/build.gradle.kts @@ -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.useLegacyMixinAp = false @@ -126,6 +124,10 @@ loom { sourceSet(sourceSets.testMod.get()) sourceSet(project(":common").sourceSets.testMod.get()) } + + register("examplemod") { + sourceSet(sourceSets.examples.get()) + } } runs { @@ -143,19 +145,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 @@ -170,7 +177,7 @@ loom { val testClient by registering { configName = "Test Client" client() - configureForGameTest(this) + configureForGameTest() runDir("run/testClient") property("cctest.tags", "client,common") @@ -179,16 +186,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()) + } } } diff --git a/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleMod.java b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleMod.java new file mode 100644 index 000000000..9a58abcd2 --- /dev/null +++ b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleMod.java @@ -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>) 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 + } +} diff --git a/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModClient.java b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModClient.java new file mode 100644 index 000000000..7b48afe66 --- /dev/null +++ b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModClient.java @@ -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 + } +} diff --git a/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModDataGenerator.java b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModDataGenerator.java new file mode 100644 index 000000000..502298971 --- /dev/null +++ b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModDataGenerator.java @@ -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 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 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 +} diff --git a/projects/fabric/src/examples/resources/fabric.mod.json b/projects/fabric/src/examples/resources/fabric.mod.json new file mode 100644 index 000000000..e0b8cf400 --- /dev/null +++ b/projects/fabric/src/examples/resources/fabric.mod.json @@ -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": "*" + } +} diff --git a/projects/forge-api/build.gradle.kts b/projects/forge-api/build.gradle.kts index 6430b1de6..ef394e1d0 100644 --- a/projects/forge-api/build.gradle.kts +++ b/projects/forge-api/build.gradle.kts @@ -7,10 +7,6 @@ plugins { id("cc-tweaked.publishing") } -java { - withJavadocJar() -} - cct.inlineProject(":common-api") dependencies { diff --git a/projects/forge/build.gradle.kts b/projects/forge/build.gradle.kts index 02115859d..c07613a4d 100644 --- a/projects/forge/build.gradle.kts +++ b/projects/forge/build.gradle.kts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MPL-2.0 import cc.tweaked.gradle.* -import net.neoforged.gradle.dsl.common.runs.run.Run +import net.neoforged.moddevgradle.dsl.RunModel plugins { id("cc-tweaked.forge") @@ -19,106 +19,122 @@ cct { allProjects.forEach { externalSources(it) } } -sourceSets { - main { - 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") - +neoForge { + val computercraft by mods.registering { cct.sourceDirectories.get().forEach { - if (it.classes) modSources.add("computercraft", it.sourceSet) - } - - dependencies { - runtime(configurations["minecraftLibrary"]) + if (it.classes) sourceSet(it.sourceSet) } } - val client by registering { - workingDirectory(file("run")) - } - - 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"]) + val computercraftDatagen by mods.registering { + cct.sourceDirectories.get().forEach { + if (it.classes) sourceSet(it.sourceSet) } + sourceSet(sourceSets.datagen.get()) } - val gameTestServer by registering { - workingDirectory(file("run/testServer")) - configureForGameTest() + val testMod by mods.registering { + sourceSet(sourceSets.testMod.get()) + sourceSet(sourceSets.testFixtures.get()) + sourceSet(project(":core").sourceSets["testFixtures"]) } - val gameTestClient by registering { - configure(runTypes.named("client")) + val exampleMod by mods.registering { + sourceSet(sourceSets.examples.get()) + } - workingDirectory(file("run/testClient")) - configureForGameTest() + runs { + 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 { - val minecraftEmbed by registering { - isCanBeResolved = false - isCanBeConsumed = false - } - named("jarJar") { extendsFrom(minecraftEmbed.get()) } + additionalRuntimeClasspath { extendsFrom(jarJar.get()) } - val minecraftLibrary by registering { - isCanBeResolved = true - isCanBeConsumed = false - extendsFrom(minecraftEmbed.get()) - } - runtimeOnly { extendsFrom(minecraftLibrary.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()) } } register("testWithIris") { @@ -136,21 +152,18 @@ dependencies { runtimeOnly(libs.bundles.externalMods.forge.runtime) { cct.exclude(this) } compileOnly(variantOf(libs.create.forge) { classifier("slim") }) { isTransitive = false } - implementation("net.neoforged:neoforge:${libs.versions.neoForge.get()}") - // 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) - "minecraftEmbed"(libs.jzlib) - + 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) - "minecraftLibrary"(libs.netty.socks) - "minecraftLibrary"(libs.netty.proxy) + 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 +176,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"))) @@ -178,15 +191,12 @@ tasks.processResources { inputs.property("modVersion", modVersion) 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))) } } tasks.jar { - archiveClassifier.set("slim") - duplicatesStrategy = DuplicatesStrategy.FAIL - // Include all classes from other projects except core. val coreSources = project(":core").sourceSets["main"] for (source in cct.sourceDirectories.get()) { @@ -203,42 +213,28 @@ tasks.sourcesJar { for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource) } -tasks.jarJar { - archiveClassifier.set("") -} - -tasks.assemble { dependsOn("jarJar") } - // Check tasks 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("runGametest") { 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) tasks.check { dependsOn(runGametest) } val runGametestClient by tasks.registering(ClientJavaExec::class) { description = "Runs client-side gametests with no mods" - copyFrom("runGameTestClient") + copyFromForge("runTestClient") tags("client") } cct.jacoco(runGametestClient) val runGametestClientWithIris by tasks.registering(ClientJavaExec::class) { description = "Runs client-side gametests with Iris" - copyFrom("runGameTestClient") + copyFromForge("runGameTestClient") tags("iris") classpath += configurations["testWithIris"] @@ -256,20 +252,15 @@ tasks.register("checkClient") { // Upload tasks 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 { publications { named("maven", MavenPublication::class) { mavenDependencies { cct.configureExcludes(this) + exclude(libs.jei.forge.get()) } } } diff --git a/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleMod.java b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleMod.java new file mode 100644 index 000000000..e84011030 --- /dev/null +++ b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleMod.java @@ -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 + } +} diff --git a/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModClient.java b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModClient.java new file mode 100644 index 000000000..23f29d847 --- /dev/null +++ b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModClient.java @@ -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 +} diff --git a/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModDataGenerator.java b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModDataGenerator.java new file mode 100644 index 000000000..4013c5ef9 --- /dev/null +++ b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModDataGenerator.java @@ -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 registries) { + var fullRegistryPatch = TurtleUpgradeProvider.makeUpgradeRegistry(registries); + pack.addProvider(o -> new DatapackBuiltinEntriesProvider(o, fullRegistryPatch, Set.of(ExampleMod.MOD_ID))); + } + // @end region=turtle_upgrades +} diff --git a/projects/forge/src/examples/resources/META-INF/neoforge.mods.toml b/projects/forge/src/examples/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..bf2ca5aff --- /dev/null +++ b/projects/forge/src/examples/resources/META-INF/neoforge.mods.toml @@ -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" diff --git a/projects/forge/src/examples/resources/pack.mcmeta b/projects/forge/src/examples/resources/pack.mcmeta new file mode 100644 index 000000000..2e879789a --- /dev/null +++ b/projects/forge/src/examples/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 15, + "description": "Example Mod" + } +} diff --git a/projects/web/src/htmlTransform/export/index.json b/projects/web/src/htmlTransform/export/index.json index b44d6e582..4bdf3b497 100644 --- a/projects/web/src/htmlTransform/export/index.json +++ b/projects/web/src/htmlTransform/export/index.json @@ -474,4 +474,4 @@ "count": 1 } } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1938b9738..838e8f308 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,14 +8,10 @@ pluginManagement { mavenCentral() gradlePluginPortal() - maven("https://maven.neoforged.net/releases") { + maven("https://maven.neoforged.net") { name = "NeoForge" content { - includeGroup("net.minecraftforge") includeGroup("net.neoforged") - includeGroup("net.neoforged.gradle") - includeModule("codechicken", "DiffPatch") - includeModule("net.covers1624", "Quack") } }