From 5bab415790d4f7bd5c48a5ba602b18639bbdfef1 Mon Sep 17 00:00:00 2001 From: JackMacWindows Date: Thu, 12 Dec 2024 00:44:25 -0500 Subject: [PATCH 1/7] Add `turtle.getItemDetail` `detailed` flag introduction date --- .../java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java | 1 + 1 file changed, 1 insertion(+) 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

From 7e2f490626a8255319a54c84e77fe7d46eaee6d1 Mon Sep 17 00:00:00 2001
From: tizu <60812901+tizu69@users.noreply.github.com>
Date: Sun, 22 Dec 2024 17:45:04 +0100
Subject: [PATCH 2/7] Show HTTP error in wget (#2037)

---
 .../data/computercraft/lua/rom/programs/http/wget.lua  | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

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.")

From 2ba6d5815b9d0d7748bcb5a05d1636268432baa6 Mon Sep 17 00:00:00 2001
From: Jonathan Coates 
Date: Tue, 31 Dec 2024 11:12:58 +0000
Subject: [PATCH 3/7] Fix fs.isDriveRoot for missing files
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

fs.getDrive returns nil for missing files, rather than the mount of the
parent file. This is a bit confusing — other mount-related functions
(e.g. getFreeSpace) find the nearest mount — but I think it's too late
to change this. Instead, we check if the file exists first.
---
 .../data/computercraft/lua/rom/apis/fs.lua    | 15 +++++++++-----
 .../computercraft/lua/rom/programs/lua.lua    |  2 +-
 .../resources/test-rom/spec/apis/fs_spec.lua  | 20 +++++++++++++++++++
 3 files changed, 31 insertions(+), 6 deletions(-)

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/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")

From ad748930581fd2ae6312681e7f99f66347bf64b3 Mon Sep 17 00:00:00 2001
From: Jonathan Coates 
Date: Sun, 5 Jan 2025 23:02:16 +0000
Subject: [PATCH 4/7] Remove redundant condition in io.read

---
 .../src/main/resources/data/computercraft/lua/rom/apis/io.lua   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

From 479aabdd097d0a061a2d1e4adbbe029cbd5720de Mon Sep 17 00:00:00 2001
From: Jonathan Coates 
Date: Sun, 5 Jan 2025 23:23:24 +0000
Subject: [PATCH 5/7] Tweaks to CONTRIBUTING.md

---
 CONTRIBUTING.md | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6ff9273e3..eac689193 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -49,9 +49,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.

From d9fc1c3a80d97a3aa1b18a997c32cd52ccfe7f14 Mon Sep 17 00:00:00 2001
From: Jonathan Coates 
Date: Sun, 5 Jan 2025 23:25:29 +0000
Subject: [PATCH 6/7] Remove mention of submitting translations via PRs

Turns out it's possible, but a bit of a faff with CrowdIn. Why is all
translation software terrible!
---
 CONTRIBUTING.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index eac689193..0ef0595be 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.

From 3c46b8acd76c834f9a50c0c85e2bd695dd3deb97 Mon Sep 17 00:00:00 2001
From: Jonathan Coates 
Date: Thu, 9 Jan 2025 20:47:51 +0000
Subject: [PATCH 7/7] Clean up Javadocs a little
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

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

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

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

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

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

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

 - Move our existing 
{@code ...}
blocks into the example mod, replacing them with {@snippet}s. - Add a new overview page to the docs, providing some getting-started information. We had this already in the dan200.computercraft.api package docs, but it's not especially visible there. --- .pre-commit-config.yaml | 1 + README.md | 13 - REUSE.toml | 19 +- buildSrc/build.gradle.kts | 17 +- .../main/kotlin/cc-tweaked.forge.gradle.kts | 23 +- .../cc-tweaked.java-convention.gradle.kts | 8 +- .../src/main/kotlin/cc-tweaked.mod.gradle.kts | 40 +-- .../cc/tweaked/gradle/CCTweakedExtension.kt | 2 +- .../cc/tweaked/gradle/ForgeExtensions.kt | 26 -- .../tweaked/gradle/MinecraftConfigurations.kt | 40 ++- .../kotlin/cc/tweaked/gradle/MinecraftExec.kt | 16 +- .../gradle/common/util/runs/RunConfigSetup.kt | 51 ---- config/checkstyle/checkstyle.xml | 2 +- gradle/libs.versions.toml | 10 +- projects/common-api/build.gradle.kts | 26 +- .../client/turtle/TurtleUpgradeModeller.java | 9 +- .../computercraft/api/ComputerCraftAPI.java | 11 +- .../api/pocket/IPocketUpgrade.java | 7 +- .../api/turtle/ITurtleUpgrade.java | 56 ++++- .../api/turtle/TurtleUpgradeDataProvider.java | 3 + .../api/turtle/TurtleUpgradeSerialiser.java | 26 -- .../api/upgrades/UpgradeDataProvider.java | 12 +- projects/common-api/src/overview.html | 68 +++++ projects/common/build.gradle.kts | 24 +- .../example_turtle_upgrade.json | 4 + .../com/example/examplemod/ExampleAPI.java | 75 ++++++ .../com/example/examplemod/ExampleMod.java | 39 +++ .../examplemod/ExampleTurtleUpgrade.java | 17 ++ .../data/ExampleModDataGenerators.java | 17 ++ .../examplemod/data/TurtleDataProvider.java | 34 +++ .../com/example/examplemod/package-info.java | 12 + .../peripheral/BrewingStandPeripheral.java | 39 +++ .../ComputerTrackingPeripheral.java | 44 ++++ .../peripheral/FurnacePeripheral.java | 29 +++ projects/core-api/build.gradle.kts | 4 - .../dan200/computercraft/api/lua/Coerced.java | 9 +- .../computercraft/api/lua/GenericSource.java | 17 +- .../computercraft/api/package-info.java | 11 - .../api/peripheral/AttachedComputerSet.java | 16 +- .../api/peripheral/package-info.java | 133 +--------- .../core/util/SanitisedError.java | 2 +- projects/fabric-api/build.gradle.kts | 4 - projects/fabric/build.gradle.kts | 46 ++-- .../example/examplemod/FabricExampleMod.java | 30 +++ .../examplemod/FabricExampleModClient.java | 14 ++ .../FabricExampleModDataGenerator.java | 17 ++ .../src/examples/resources/fabric.mod.json | 16 ++ projects/forge-api/build.gradle.kts | 12 - projects/forge/build.gradle.kts | 234 ++++++++---------- .../example/examplemod/ForgeExampleMod.java | 92 +++++++ .../examplemod/ForgeExampleModClient.java | 20 ++ .../ForgeExampleModDataGenerator.java | 19 ++ .../src/examples/resources/META-INF/mods.toml | 14 ++ .../forge/src/examples/resources/pack.mcmeta | 6 + .../cc/tweaked/javadoc/SnippetTaglet.kt | 138 +++++++++++ .../web/src/htmlTransform/export/index.json | 2 +- settings.gradle.kts | 29 +-- 57 files changed, 1089 insertions(+), 616 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt delete mode 100644 buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt create mode 100644 projects/common-api/src/overview.html create mode 100644 projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrades/example_turtle_upgrade.json create mode 100644 projects/common/src/examples/java/com/example/examplemod/ExampleAPI.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/ExampleMod.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/ExampleTurtleUpgrade.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/data/ExampleModDataGenerators.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/data/TurtleDataProvider.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/package-info.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/peripheral/BrewingStandPeripheral.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/peripheral/ComputerTrackingPeripheral.java create mode 100644 projects/common/src/examples/java/com/example/examplemod/peripheral/FurnacePeripheral.java create mode 100644 projects/fabric/src/examples/java/com/example/examplemod/FabricExampleMod.java create mode 100644 projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModClient.java create mode 100644 projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModDataGenerator.java create mode 100644 projects/fabric/src/examples/resources/fabric.mod.json create mode 100644 projects/forge/src/examples/java/com/example/examplemod/ForgeExampleMod.java create mode 100644 projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModClient.java create mode 100644 projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModDataGenerator.java create mode 100644 projects/forge/src/examples/resources/META-INF/mods.toml create mode 100644 projects/forge/src/examples/resources/pack.mcmeta create mode 100644 projects/lints/src/main/kotlin/cc/tweaked/javadoc/SnippetTaglet.kt 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/README.md b/README.md index 3b1859631..46397915f 100644 --- a/README.md +++ b/README.md @@ -61,19 +61,6 @@ dependencies { } ``` -When using ForgeGradle, you may also need to add the following: - -```groovy -minecraft { - runs { - configureEach { - property 'mixin.env.remapRefMap', 'true' - property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg" - } - } -} -``` - You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are subject to change at any point. If you depend on functionality outside the API (or need to mixin to CC:T), please file an issue to let me know! diff --git a/REUSE.toml b/REUSE.toml index 0c43beba1..88259ac93 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]] @@ -46,7 +51,6 @@ path = [ "projects/fabric/src/main/resources/fabric.mod.json", "projects/fabric/src/testMod/resources/computercraft-gametest.fabric.mixins.json", "projects/fabric/src/testMod/resources/fabric.mod.json", - "projects/forge/src/client/resources/computercraft-client.forge.mixins.json", "projects/web/src/frontend/mount/.settings", "projects/web/src/frontend/mount/example.nfp", "projects/web/src/frontend/mount/example.nft", @@ -73,7 +77,7 @@ path = [ ] [[annotations]] -# Community-contributed license files +# Community-contributed language files SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" SPDX-License-Identifier = "LicenseRef-CCPL" path = [ @@ -87,18 +91,11 @@ path = [ ] [[annotations]] -# Community-contributed license files +# Community-contributed language files SPDX-FileCopyrightText = "2017 The CC: Tweaked Developers" SPDX-License-Identifier = "MPL-2.0" path = "projects/common/src/main/resources/assets/computercraft/lang/**" -[[annotations]] -# GitHub build scripts are CC0. While we could add a header to each file, -# it's unclear if it will break actions or issue templates in some way. -SPDX-FileCopyrightText = "Jonathan Coates " -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 90b6c2391..e03247564 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -14,18 +14,10 @@ repositories { mavenCentral() gradlePluginPortal() - maven("https://maven.minecraftforge.net") { - name = "Forge" + maven("https://maven.neoforged.net") { + name = "NeoForge" content { - includeGroup("net.minecraftforge") - includeGroup("net.minecraftforge.gradle") - } - } - - maven("https://maven.parchmentmc.org") { - name = "Librarian" - content { - includeGroupByRegex("^org\\.parchmentmc.*") + includeGroup("net.neoforged") } } @@ -50,10 +42,9 @@ dependencies { implementation(libs.spotless) implementation(libs.fabric.loom) - implementation(libs.forgeGradle) implementation(libs.ideaExt) - implementation(libs.librarian) implementation(libs.minotaur) + implementation(libs.modDevGradle) implementation(libs.vanillaExtract) } diff --git a/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts index ece233e6f..67cc1d5c0 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.forge.gradle.kts @@ -10,21 +10,22 @@ import cc.tweaked.gradle.IdeaRunConfigurations import cc.tweaked.gradle.MinecraftConfigurations plugins { - id("net.minecraftforge.gradle") - // We must apply java-convention after Forge, as we need the fg extension to be present. id("cc-tweaked.java-convention") - id("org.parchmentmc.librarian.forgegradle") + id("net.neoforged.moddev.legacyforge") } plugins.apply(CCTweakedPlugin::class.java) val mcVersion: String by extra -minecraft { +legacyForge { val libs = project.extensions.getByType().named("libs") - mappings("parchment", "${libs.findVersion("parchmentMc").get()}-${libs.findVersion("parchment").get()}-$mcVersion") + version = "${mcVersion}-${libs.findVersion("forge").get()}" - accessTransformer(project(":forge").file("src/main/resources/META-INF/accesstransformer.cfg")) + parchment { + minecraftVersion = libs.findVersion("parchmentMc").get().toString() + mappingsVersion = libs.findVersion("parchment").get().toString() + } } MinecraftConfigurations.setup(project) @@ -32,13 +33,3 @@ MinecraftConfigurations.setup(project) extensions.configure(CCTweakedExtension::class.java) { linters(minecraft = true, loader = "forge") } - -dependencies { - val libs = project.extensions.getByType().named("libs") - "minecraft"("net.minecraftforge:forge:$mcVersion-${libs.findVersion("forge").get()}") -} - -tasks.configureEach { - // genIntellijRuns isn't registered until much later, so we need this silly hijinks. - if (name == "genIntellijRuns") doLast { IdeaRunConfigurations(project).patch() } -} 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 e0bd006ec..71da328d0 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -44,13 +44,6 @@ repositories { exclusiveContent { forRepositories(mainMaven) - - // Include the ForgeGradle repository if present. This requires that ForgeGradle is already present, which we - // enforce in our Forge overlay. - val fg = - project.extensions.findByType(net.minecraftforge.gradle.userdev.DependencyManagementExtension::class.java) - if (fg != null) forRepositories(fg.repository) - filter { includeGroup("cc.tweaked") // Things we mirror @@ -99,6 +92,7 @@ sourceSets.all { check("OperatorPrecedence", CheckSeverity.OFF) // For now. check("NonOverridingEquals", CheckSeverity.OFF) // Peripheral.equals makes this hard to avoid check("FutureReturnValueIgnored", CheckSeverity.OFF) // Too many false positives with Netty + check("InvalidInlineTag", CheckSeverity.OFF) // Triggered by @snippet. Can be removed on Java 21. check("NullAway", CheckSeverity.ERROR) option( 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/ForgeExtensions.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt deleted file mode 100644 index bbf6e86c5..000000000 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/ForgeExtensions.kt +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package cc.tweaked.gradle - -import net.minecraftforge.gradle.common.util.RunConfig -import net.minecraftforge.gradle.common.util.runs.setRunConfigInternal -import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.tasks.JavaExec -import org.gradle.jvm.toolchain.JavaToolchainService -import java.nio.file.Files - -/** - * Set [JavaExec] task to run a given [RunConfig]. - */ -fun JavaExec.setRunConfig(config: RunConfig) { - dependsOn("prepareRuns") - setRunConfigInternal(project, this, config) - doFirst("Create working directory") { Files.createDirectories(workingDir.toPath()) } - - javaLauncher.set( - project.extensions.getByType(JavaToolchainService::class.java) - .launcherFor(project.extensions.getByType(JavaPluginExtension::class.java).toolchain), - ) -} 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 f923383c5..fecae4c53 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt @@ -4,7 +4,7 @@ package cc.tweaked.gradle -import net.minecraftforge.gradle.common.util.RunConfig +import net.neoforged.moddevgradle.internal.RunGameTask import org.gradle.api.GradleException import org.gradle.api.file.FileSystemOperations import org.gradle.api.invocation.Gradle @@ -65,11 +65,19 @@ abstract class ClientJavaExec : JavaExec() { setTestProperties() } + fun copyFromForge(path: String) = copyFromForge(project.tasks.getByName(path, RunGameTask::class)) + /** - * Set this task to run a given [RunConfig]. + * Set this task to run a given [RunGameTask]. */ - fun setRunConfig(config: RunConfig) { - (this as JavaExec).setRunConfig(config) + fun copyFromForge(task: RunGameTask) { + copyFrom(task) + + // Eagerly evaluate the behaviour of RunGameTask.exec + environment.putAll(task.environmentProperty.get()) + classpath(task.classpathProvider) + workingDir = task.gameDirectory.get().asFile + setTestProperties() // setRunConfig may clobber some properties, ensure everything is set. } diff --git a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt b/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt deleted file mode 100644 index a74d0008d..000000000 --- a/buildSrc/src/main/kotlin/net/minecraftforge/gradle/common/util/runs/RunConfigSetup.kt +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package net.minecraftforge.gradle.common.util.runs - -import net.minecraftforge.gradle.common.util.RunConfig -import org.gradle.api.Project -import org.gradle.process.CommandLineArgumentProvider -import org.gradle.process.JavaExecSpec -import java.io.File -import java.util.function.Supplier -import java.util.stream.Collectors -import java.util.stream.Stream - -/** - * Set up a [JavaExecSpec] to execute a [RunConfig]. - * - * [MinecraftRunTask] sets up all its properties when the task is executed, rather than when configured. As such, it's - * not possible to use [cc.tweaked.gradle.copyToFull] like we do for Fabric. Instead, we set up the task manually. - * - * Unfortunately most of the functionality we need is package-private, and so we have to put our code into the package. - */ -internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: RunConfig) { - spec.workingDir = File(config.workingDirectory) - - spec.mainClass.set(config.main) - for (source in config.allSources) spec.classpath(source.runtimeClasspath) - - val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java) - - // Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts. - val lazyTokens = RunConfigGenerator.configureTokensLazy( - project, config, RunConfigGenerator.mapModClassesToGradle(project, config), - originalTask.get().minecraftArtifacts, - originalTask.get().runtimeClasspathArtifacts, - ) - spec.argumentProviders.add( - CommandLineArgumentProvider { - RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList() - }, - ) - spec.jvmArgumentProviders.add( - CommandLineArgumentProvider { - (if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } + - config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" } - }, - ) - - for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value)) -} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index bf8face88..c8a0ae25e 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -124,7 +124,7 @@ SPDX-License-Identifier: MPL-2.0 - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ada42638e..1152fe2db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,14 +61,13 @@ checkstyle = "10.14.1" errorProne-core = "2.27.0" errorProne-plugin = "3.1.0" fabric-loom = "1.7.1" -forgeGradle = "6.0.21" githubRelease = "2.5.2" gradleVersions = "0.50.0" ideaExt = "1.1.7" illuaminate = "0.1.0-74-gf1551d5" -librarian = "1.+" lwjgl = "3.3.3" -minotaur = "2.+" +minotaur = "2.8.7" +modDevGradle = "2.0.74" nullAway = "0.10.25" shadow = "8.3.1" spotless = "6.23.3" @@ -150,12 +149,11 @@ errorProne-core = { module = "com.google.errorprone:error_prone_core", version.r errorProne-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorProne-plugin" } errorProne-testHelpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "errorProne-core" } fabric-loom = { module = "net.fabricmc:fabric-loom", version.ref = "fabric-loom" } -forgeGradle = { module = "net.minecraftforge.gradle:ForgeGradle", version.ref = "forgeGradle" } ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "ideaExt" } kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" } minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" } nullAway = { module = "com.uber.nullaway:nullaway", version.ref = "nullAway" } +modDevGradle = { module = "net.neoforged:moddev-gradle", version.ref = "modDevGradle" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } teavm-classlib = { module = "org.teavm:teavm-classlib", version.ref = "teavm" } teavm-core = { module = "org.teavm:teavm-core", version.ref = "teavm" } @@ -170,11 +168,9 @@ vanillaExtract = { module = "cc.tweaked.vanilla-extract:plugin", version.ref = " yarn = { module = "net.fabricmc:yarn", version.ref = "yarn" } [plugins] -forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" } githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" } gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" } kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" } versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" } diff --git a/projects/common-api/build.gradle.kts b/projects/common-api/build.gradle.kts index b00eb56db..c63e105f7 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,15 @@ tasks.javadoc { """.trimIndent(), ) + + taglets("cc.tweaked.javadoc.SnippetTaglet") + tagletPath(configurations.detachedConfiguration(dependencies.project(":lints")).toList()) + + val snippetSources = listOf(":common", ":fabric", ":forge").flatMap { + project(it).sourceSets["examples"].allSource.sourceDirectories + } + inputs.files(snippetSources) + jFlags("-Dcc.snippet-path=" + snippetSources.joinToString(File.pathSeparator) { it.absolutePath }) } // Include the core-api in our javadoc export. This is wrong, but it means we can export a single javadoc dump. 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 7623f7d30..a453723a7 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 @@ -22,7 +22,14 @@ import java.util.List; *

* 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 3f57e43ad..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: * - *

{@code
-     * ComputerCraftAPI.registerAPIFactory(computer -> {
-     *   // Read the turtle component.
-     *   var turtle = computer.getComponent(ComputerComponents.TURTLE);
-     *   // If present then add our API.
-     *   return turtle == null ? null : new MyCustomTurtleApi(turtle);
-     * });
-     * }
+ * {@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 1cf22e2d4..2ee590466 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 @@ -14,13 +14,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, on creates a {@link IPocketUpgrade} subclass and corresponding - * {@link PocketUpgradeSerialiser} instance, which are then registered in a Forge registry. + * {@link PocketUpgradeSerialiser} instance, which are then registered in a Minecraft registry. *

* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and - * the upgrade registered internally. See the documentation in {@link PocketUpgradeSerialiser} for details on this process - * and where files should be located. - * - * @see PocketUpgradeSerialiser For how to register a pocket computer upgrade. + * the upgrade registered internally. */ public interface IPocketUpgrade extends UpgradeBase { /** 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 2e4d7616b..eb776306f 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 @@ -8,21 +8,69 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.upgrades.UpgradeBase; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.Items; import javax.annotation.Nullable; +import java.util.function.BiFunction; /** * The primary interface for defining an update for Turtles. A turtle update can either be a new tool, or a new * peripheral. *

* Turtle upgrades are defined in two stages. First, one creates a {@link ITurtleUpgrade} subclass and corresponding - * {@link TurtleUpgradeSerialiser} instance, which are then registered in a Forge registry. + * {@link TurtleUpgradeSerialiser} instance, which are then registered in a Minecraft registry. *

* You then write a JSON file in your mod's {@literal data/} folder. This is then parsed when the world is loaded, and - * the upgrade registered internally. See the documentation in {@link TurtleUpgradeSerialiser} for details on this process - * and where files should be located. + * the upgrade automatically registered. * - * @see TurtleUpgradeSerialiser For how to register a turtle upgrade. + *

Example

+ *

Registering the upgrade serialiser

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

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

+ * Now we must construct a new upgrade serialiser. In most cases, you can use one of the helper methods + * (e.g. {@link TurtleUpgradeSerialiser#simpleWithCustomItem(BiFunction)}), rather than defining your own implementation. + * + * {@snippet class=com.example.examplemod.ExampleMod region=turtle_upgrades} + * + * We now must register this upgrade serialiser. This is done the same way as you'd register blocks, items, or other + * Minecraft objects. The approach to do this will depend on mod-loader. + * + *

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

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_upgrades/.json}. + * + * {@snippet file = data/examplemod/computercraft/turtle_upgrades/example_turtle_upgrade.json} + * + * The {@code "type"} field points to the ID of the upgrade serialiser we've just registered, while the other fields + * are read by the serialiser itself. As our upgrade was defined with {@link TurtleUpgradeSerialiser#simpleWithCustomItem(BiFunction)}, the + * {@code "item"} field will construct our upgrade with {@link Items#COMPASS}. + *

+ * Rather than manually creating the file, it is recommended to data-generators to generate this file. This can be done + * with {@link TurtleUpgradeDataProvider}. + * + * {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body} + * + * @see TurtleUpgradeSerialiser Registering a turtle upgrade. */ public interface ITurtleUpgrade extends UpgradeBase { /** diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java index 1a8118544..84753db98 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeDataProvider.java @@ -29,6 +29,9 @@ import java.util.function.Consumer; * {@link #addUpgrades(Consumer)} function, construct each upgrade, and pass them off to the provided consumer to * generate them. * + *

Example

+ * {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body} + * * @see TurtleUpgradeSerialiser */ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider> { diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeSerialiser.java b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeSerialiser.java index 6f9d2a7af..809cbf1ba 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeSerialiser.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/turtle/TurtleUpgradeSerialiser.java @@ -27,32 +27,6 @@ import java.util.function.Function; * If your turtle upgrade doesn't have any associated configurable parameters (like most upgrades), you can use * {@link #simple(Function)} or {@link #simpleWithCustomItem(BiFunction)} to create a basic upgrade serialiser. * - *

Example (Forge)

- *
{@code
- * static final DeferredRegister> SERIALISERS = DeferredRegister.create( TurtleUpgradeSerialiser.TYPE, "my_mod" );
- *
- * // Register a new upgrade serialiser called "my_upgrade".
- * public static final RegistryObject> MY_UPGRADE =
- *     SERIALISERS.register( "my_upgrade", () -> TurtleUpgradeSerialiser.simple( MyUpgrade::new ) );
- *
- * // Then in your constructor
- * SERIALISERS.register( bus );
- * }
- *

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

{@code
- * {
- *     "type": "my_mod:my_upgrade",
- * }
- * }
- *

- * Finally, we need to register a model for our upgrade. The way to do this varies on mod loader, see - * {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModeller} for more information. - *

- * {@link TurtleUpgradeDataProvider} provides a data provider to aid with generating these JSON files. - * * @param The type of turtle upgrade this is responsible for serialising. * @see ITurtleUpgrade * @see TurtleUpgradeDataProvider diff --git a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeDataProvider.java b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeDataProvider.java index b405a1cf6..2d691ebeb 100644 --- a/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeDataProvider.java +++ b/projects/common-api/src/main/java/dan200/computercraft/api/upgrades/UpgradeDataProvider.java @@ -32,6 +32,8 @@ import java.util.function.Function; * * @param The base class of upgrades. * @param The upgrade serialiser to register for. + * @see dan200.computercraft.api.turtle.TurtleUpgradeDataProvider + * @see dan200.computercraft.api.pocket.PocketUpgradeDataProvider */ public abstract class UpgradeDataProvider> implements DataProvider { private final PackOutput output; @@ -84,13 +86,9 @@ public abstract class UpgradeDataProvider - * Example usage: - *

{@code
-     * protected void addUpgrades(Consumer>> addUpgrade) {
-     *     simple(new ResourceLocation("mymod", "speaker"), SPEAKER_SERIALISER.get()).add(addUpgrade);
-     * }
-     * }
+ * + *

Example

+ * {@snippet class=com.example.examplemod.data.TurtleDataProvider region=body} * * @param addUpgrade A callback used to register an upgrade. */ 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 7826e59da..eb1e320e1 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", @@ -113,20 +107,28 @@ val lintLua by tasks.registering(IlluaminateExec::class) { doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") } } -val runData by tasks.registering(MergeTrees::class) { - output = layout.projectDirectory.dir("src/generated/resources") +fun MergeTrees.configureForDatagen(source: SourceSet, outputFolder: String) { + output = layout.projectDirectory.dir(outputFolder) for (loader in listOf("forge", "fabric")) { - mustRunAfter(":$loader:runData") + mustRunAfter(":$loader:$name") source { input { - from(project(":$loader").layout.buildDirectory.dir("generatedResources")) + from(project(":$loader").layout.buildDirectory.dir(source.getTaskName("generateResources", null))) exclude(".cache") } - output = project(":$loader").layout.projectDirectory.dir("src/generated/resources") + output = project(":$loader").layout.projectDirectory.dir(outputFolder) } } } +val runData by tasks.registering(MergeTrees::class) { + configureForDatagen(sourceSets.main.get(), "src/generated/resources") +} + +val runExampleData by tasks.registering(MergeTrees::class) { + configureForDatagen(sourceSets.examples.get(), "src/examples/generatedResources") +} + tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false } diff --git a/projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrades/example_turtle_upgrade.json b/projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrades/example_turtle_upgrade.json new file mode 100644 index 000000000..5aa5c5b76 --- /dev/null +++ b/projects/common/src/examples/generatedResources/data/examplemod/computercraft/turtle_upgrades/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..df15779a2 --- /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.TurtleDataProvider; +import com.example.examplemod.peripheral.FurnacePeripheral; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; + +/** + * Our example mod, containing the various things we register. + *

+ * 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 serialiser for our example turtle upgrade. See the documentation for {@link TurtleUpgradeSerialiser} + * or {@code FabricExampleMod}/{@code ForgeExampleMod} for how this is registered. + *

+ * This only defines the upgrade type. See {@link TurtleDataProvider} for defining the actual upgrade. + */ + // @start region=turtle_upgrades + public static final TurtleUpgradeSerialiser EXAMPLE_TURTLE_UPGRADE = TurtleUpgradeSerialiser.simpleWithCustomItem( + ExampleTurtleUpgrade::new + ); + // @end region=turtle_upgrades + + public static void registerComputerCraft() { + // @start region=generic_source + ComputerCraftAPI.registerGenericSource(new FurnacePeripheral()); + // @end region=generic_source + + ExampleAPI.register(); + } +} 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..fed15a065 --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/ExampleTurtleUpgrade.java @@ -0,0 +1,17 @@ +package com.example.examplemod; + +import dan200.computercraft.api.turtle.AbstractTurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleUpgradeType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +/** + * An example turtle upgrade. + */ +// @start region=body +public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade { + public ExampleTurtleUpgrade(ResourceLocation id, ItemStack stack) { + super(id, TurtleUpgradeType.PERIPHERAL, stack); + } +} +// @end region=body diff --git a/projects/common/src/examples/java/com/example/examplemod/data/ExampleModDataGenerators.java b/projects/common/src/examples/java/com/example/examplemod/data/ExampleModDataGenerators.java new file mode 100644 index 000000000..0cda1c4e2 --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/data/ExampleModDataGenerators.java @@ -0,0 +1,17 @@ +package com.example.examplemod.data; + +import net.minecraft.data.DataGenerator; +import net.minecraft.data.DataProvider; + +/** + * The entry point to example mod's data-generators. + *

+ * This is called by our platform-specific entry-point (see {@code FabricExampleModDataGenerator} and + * {@code ForgeExampleModDataGenerator}. That said, the exact setup isn't relevant (it will vary depending on + * mod-loader), what's interesting is the contents of the {@link #run(DataGenerator.PackGenerator)} method! + */ +public final class ExampleModDataGenerators { + public static void run(DataGenerator.PackGenerator pack) { + pack.addProvider((DataProvider.Factory) TurtleDataProvider::new); + } +} diff --git a/projects/common/src/examples/java/com/example/examplemod/data/TurtleDataProvider.java b/projects/common/src/examples/java/com/example/examplemod/data/TurtleDataProvider.java new file mode 100644 index 000000000..d901daaec --- /dev/null +++ b/projects/common/src/examples/java/com/example/examplemod/data/TurtleDataProvider.java @@ -0,0 +1,34 @@ +package com.example.examplemod.data; + +import com.example.examplemod.ExampleMod; +import com.example.examplemod.ExampleTurtleUpgrade; +import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider; +import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; +import net.minecraft.data.PackOutput; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Items; + +import java.util.function.Consumer; + +/** + * A {@link TurtleUpgradeDataProvider} that generates the JSON for our {@linkplain ExampleTurtleUpgrade example + * upgrade}. + * + * @see ExampleModDataGenerators + */ +// @start region=body +public class TurtleDataProvider extends TurtleUpgradeDataProvider { + public TurtleDataProvider(PackOutput output) { + super(output); + } + + @Override + protected void addUpgrades(Consumer>> addUpgrade) { + simpleWithCustomItem( + new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"), + ExampleMod.EXAMPLE_TURTLE_UPGRADE, + Items.COMPASS + ).add(addUpgrade); + } +} +// @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..34054ec28 --- /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().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..f1c873818 --- /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 new ResourceLocation(ExampleMod.MOD_ID, "furnace").toString(); + } + + @LuaFunction(mainThread = true) + public int getBurnTime(AbstractFurnaceBlockEntity furnace) { + // Don't do it this way! Use an access widener/transformer to access the "litTime" field instead. + return furnace.saveWithoutMetadata().getInt("BurnTime"); + } +} +// @end region=body 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 1af2cda21..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:

- *
{@code
- * @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 09b8ff9d6..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: * - *

{@code
- * 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 e5fcd589b..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

* - *
{@code
- * public class MyPeripheral implements IPeripheral {
- *     private final AttachedComputerSet computers = new ComputerCollection();
- *
- *     @Override
- *     public void attach(IComputerAccess computer) {
- *         computers.add(computer);
- *     }
- *
- *     @Override
- *     public void detach(IComputerAccess computer) {
- *         computers.remove(computer);
- *     }
- * }
- * }
+ * {@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 dfc48e670..34ee1a326 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 @@ -48,35 +48,11 @@ * argument, which in this case is a {@code AbstractFurnaceBlockEntity}. We then annotate this method with * {@link dan200.computercraft.api.lua.LuaFunction} to expose it to computers. * - *
{@code
- * import dan200.computercraft.api.lua.LuaFunction;
- * import dan200.computercraft.api.peripheral.GenericPeripheral;
- * import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
+ * {@snippet class=com.example.examplemod.peripheral.FurnacePeripheral region=body}
  *
- * public final class FurnacePeripheral implements GenericPeripheral {
- *     @Override
- *     public String id() {
- *         return "mymod:furnace";
- *     }
- *
- *     @LuaFunction(mainThread = true)
- *     public int getBurnTime(AbstractFurnaceBlockEntity furnace) {
- *         return furnace.litTime;
- *     }
- * }
- * }
- *

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

{@code
- * 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 @@ -85,36 +61,8 @@ * We can then start adding peripheral methods to our class. Each method should be {@code final}, and annotated with * {@link dan200.computercraft.api.lua.LuaFunction}. * - *
{@code
- * import dan200.computercraft.api.lua.LuaFunction;
- * import dan200.computercraft.api.peripheral.IPeripheral;
- * import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
- * import org.jetbrains.annotations.Nullable;
+ * {@snippet class=com.example.examplemod.peripheral.BrewingStandPeripheral region=body}
  *
- * public class FurnacePeripheral implements IPeripheral {
- *     private final AbstractFurnaceBlockEntity furnace;
- *
- *     public FurnacePeripheral(AbstractFurnaceBlockEntity furnace) {
- *         this.furnace = furnace;
- *     }
- *
- *     @Override
- *     public String getType() {
- *         return "furnace";
- *     }
- *
- *     @LuaFunction(mainThread = true)
- *     public final int getBurnTime() {
- *         return furnace.litTime;
- *     }
- *
- *     @Override
- *     public boolean equals(@Nullable IPeripheral other) {
- *         return this == other || other instanceof FurnacePeripheral p && furnace == p.furnace;
- *     }
- * }
- * }
- *

* Finally, we'll need to register our peripheral. This is done with capabilities on Forge, or the block lookup API on * Fabric. * @@ -124,82 +72,11 @@ * {@code ICapabilityProvider}. If you've got an existing system for dealing with this, we recommend you use that, * otherwise you can use something similar to the code below: * - *

{@code
- * import dan200.computercraft.api.peripheral.IPeripheral;
- * import net.minecraft.core.Direction;
- * import net.minecraft.resources.ResourceLocation;
- * import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
- * import net.minecraft.world.level.block.entity.BlockEntity;
- * import net.minecraftforge.common.capabilities.Capability;
- * import net.minecraftforge.common.capabilities.CapabilityManager;
- * import net.minecraftforge.common.capabilities.CapabilityToken;
- * import net.minecraftforge.common.capabilities.ICapabilityProvider;
- * import net.minecraftforge.common.util.LazyOptional;
- * import net.minecraftforge.event.AttachCapabilitiesEvent;
- * import org.jetbrains.annotations.Nullable;
- *
- * import java.util.function.Function;
- *
- * public class ComputerCraftCompat {
- *     public static final Capability CAPABILITY_PERIPHERAL = CapabilityManager.get(new CapabilityToken<>() {
- *     });
- *     private static final ResourceLocation PERIPHERAL = new ResourceLocation("mymod", "peripheral");
- *
- *     public static void register(AttachCapabilitiesEvent event) {
- *         if (event.getObject() instanceof AbstractFurnaceBlockEntity furnace) {
- *             PeripheralProvider.attach(event, furnace, FurnacePeripheral::new);
- *         }
- *     }
- *
- *     // A {@link ICapabilityProvider} that lazily creates an {@link IPeripheral} when required.
- *     private static class PeripheralProvider implements ICapabilityProvider {
- *         private final O blockEntity;
- *         private final Function factory;
- *         private @Nullable LazyOptional peripheral;
- *
- *         private PeripheralProvider(O blockEntity, Function factory) {
- *             this.blockEntity = blockEntity;
- *             this.factory = factory;
- *         }
- *
- *         private static  void attach(AttachCapabilitiesEvent event, O blockEntity, Function factory) {
- *             var provider = new PeripheralProvider<>(blockEntity, factory);
- *             event.addCapability(PERIPHERAL, provider);
- *             event.addListener(provider::invalidate);
- *         }
- *
- *         private void invalidate() {
- *             if (peripheral != null) peripheral.invalidate();
- *             peripheral = null;
- *         }
- *
- *         @Override
- *         public  LazyOptional getCapability(Capability capability, @Nullable Direction direction) {
- *             if (capability != CAPABILITY_PERIPHERAL) return LazyOptional.empty();
- *             if (blockEntity.isRemoved()) return LazyOptional.empty();
- *
- *             var peripheral = this.peripheral;
- *             return (peripheral == null ? (this.peripheral = LazyOptional.of(() -> factory.apply(blockEntity))) : peripheral).cast();
- *         }
- *     }
- * }
- * }
+ * {@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}. * - *
{@code
- * import dan200.computercraft.api.peripheral.PeripheralLookup;
- * import dan200.computercraft.example.FurnacePeripheral;
- * import net.minecraft.world.level.block.entity.BlockEntityType;
- *
- * public class ComputerCraftCompat {
- *     public static void register() {
- *         PeripheralLookup.get().registerForBlockEntity((f, s) -> new FurnacePeripheral(f), BlockEntityType.FURNACE);
- *         PeripheralLookup.get().registerForBlockEntity((f, s) -> new FurnacePeripheral(f), BlockEntityType.BLAST_FURNACE);
- *         PeripheralLookup.get().registerForBlockEntity((f, s) -> new FurnacePeripheral(f), BlockEntityType.SMOKER);
- *     }
- * }
- * }
+ * {@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/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 4896ef955..67b89e09c 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.defaultRefmapName.set("computercraft.refmap.json") @@ -126,6 +124,10 @@ loom { sourceSet(sourceSets.testMod.get()) sourceSet(project(":common").sourceSets.testMod.get()) } + + register("examplemod") { + sourceSet(sourceSets.examples.get()) + } } runs { @@ -142,19 +144,24 @@ loom { runDir("run/server") } - register("data") { - configName = "Datagen" + fun RunConfigSettings.configureForData(sourceSet: SourceSet) { client() - - source(sourceSets.datagen.get()) - - runDir("run/dataGen") + runDir("run/run${name.capitalise()}") property("fabric-api.datagen") - property("fabric-api.datagen.output-dir", layout.buildDirectory.dir("generatedResources").getAbsolutePath()) + property( + "fabric-api.datagen.output-dir", + layout.buildDirectory.dir(sourceSet.getTaskName("generateResources", null)).getAbsolutePath(), + ) property("fabric-api.datagen.strict-validation") } - fun configureForGameTest(config: RunConfigSettings) = config.run { + register("data") { + configName = "Datagen" + configureForData(sourceSets.main.get()) + source(sourceSets.datagen.get()) + } + + fun RunConfigSettings.configureForGameTest() { source(sourceSets.testMod.get()) val testSources = project(":common").file("src/testMod/resources/data/cctest").absolutePath @@ -169,7 +176,7 @@ loom { val testClient by registering { configName = "Test Client" client() - configureForGameTest(this) + configureForGameTest() runDir("run/testClient") property("cctest.tags", "client,common") @@ -178,16 +185,27 @@ loom { register("gametest") { configName = "Game Test" server() - configureForGameTest(this) + configureForGameTest() property("fabric-api.gametest") property( "fabric-api.gametest.report-file", - layout.buildDirectory.dir("test-results/runGametest.xml") - .getAbsolutePath(), + layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(), ) runDir("run/gametest") } + + register("exampleClient") { + client() + configName = "Example Mod Client" + source(sourceSets.examples.get()) + } + + register("exampleData") { + configName = "Example Mod Datagen" + configureForData(sourceSets.examples.get()) + source(sourceSets.examples.get()) + } } } 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..cadc96330 --- /dev/null +++ b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleMod.java @@ -0,0 +1,30 @@ +package com.example.examplemod; + +import com.example.examplemod.peripheral.BrewingStandPeripheral; +import dan200.computercraft.api.peripheral.PeripheralLookup; +import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; +import net.fabricmc.api.ModInitializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntityType; + +/** + * The main entry point for our example mod. + */ +public class FabricExampleMod implements ModInitializer { + @Override + public void onInitialize() { + // @start region=turtle_upgrades + @SuppressWarnings("unchecked") + var turtleUpgradeSerialisers = (Registry>) BuiltInRegistries.REGISTRY.get(TurtleUpgradeSerialiser.registryId().location()); + Registry.register(turtleUpgradeSerialisers, new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"), ExampleMod.EXAMPLE_TURTLE_UPGRADE); + // @end region=turtle_upgrades + + ExampleMod.registerComputerCraft(); + + // @start region=peripherals + PeripheralLookup.get().registerForBlockEntity((f, s) -> new BrewingStandPeripheral(f), BlockEntityType.BREWING_STAND); + // @end region=peripherals + } +} 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..5ef38787d --- /dev/null +++ b/projects/fabric/src/examples/java/com/example/examplemod/FabricExampleModDataGenerator.java @@ -0,0 +1,17 @@ +package com.example.examplemod; + +import com.example.examplemod.data.TurtleDataProvider; +import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; +import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; +import net.minecraft.data.DataProvider; + +/** + * The data generator entrypoint for our Fabric example mod. + */ +public class FabricExampleModDataGenerator implements DataGeneratorEntrypoint { + @Override + public void onInitializeDataGenerator(FabricDataGenerator generator) { + var pack = generator.createPack(); + pack.addProvider((DataProvider.Factory) TurtleDataProvider::new); + } +} 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 1b169f3f4..9582164f8 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 { @@ -20,11 +16,3 @@ dependencies { tasks.javadoc { include("dan200/computercraft/api/**/*.java") } - -publishing { - publications { - named("maven", MavenPublication::class) { - fg.component(this) - } - } -} diff --git a/projects/forge/build.gradle.kts b/projects/forge/build.gradle.kts index 7caf8de4a..4b45dfb0a 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.minecraftforge.gradle.common.util.RunConfig +import net.neoforged.moddevgradle.dsl.RunModel plugins { id("cc-tweaked.forge") @@ -19,105 +19,122 @@ cct { allProjects.forEach { externalSources(it) } } -sourceSets { - main { - resources.srcDir("src/generated/resources") +legacyForge { + val computercraft by mods.registering { + cct.sourceDirectories.get().forEach { + if (it.classes) sourceSet(it.sourceSet) + } + } + + val computercraftDatagen by mods.registering { + cct.sourceDirectories.get().forEach { + if (it.classes) sourceSet(it.sourceSet) + } + sourceSet(sourceSets.datagen.get()) + } + + val testMod by mods.registering { + sourceSet(sourceSets.testMod.get()) + sourceSet(sourceSets.testFixtures.get()) + sourceSet(project(":core").sourceSets["testFixtures"]) + } + + val exampleMod by mods.registering { + sourceSet(sourceSets.examples.get()) } -} -minecraft { runs { - // configureEach would be better, but we need to eagerly configure configs or otherwise the run task doesn't - // get set up properly. - all { - property("forge.logging.markers", "REGISTRIES") - property("forge.logging.console.level", "debug") - - mods.register("computercraft") { - cct.sourceDirectories.get().forEach { - if (it.classes) sources(it.sourceSet) - } - } + configureEach { + ideName = "Forge - ${name.capitalise()}" + systemProperty("forge.logging.markers", "REGISTRIES") + systemProperty("forge.logging.console.level", "debug") + loadedMods.add(computercraft) } - val client by registering { - workingDirectory(file("run")) + register("client") { + client() } - val server by registering { - workingDirectory(file("run/server")) - arg("--nogui") + register("server") { + server() + gameDirectory = file("run/server") + programArgument("--nogui") } - val data by registering { - workingDirectory(file("run")) - args( - "--mod", "computercraft", "--all", - "--output", layout.buildDirectory.dir("generatedResources").getAbsolutePath(), - "--existing", project(":common").file("src/main/resources/"), - "--existing", file("src/main/resources/"), + fun RunModel.configureForData(mod: String, sourceSet: SourceSet) { + data() + gameDirectory = file("run/run${name.capitalise()}") + programArguments.addAll( + "--mod", mod, "--all", + "--output", + layout.buildDirectory.dir(sourceSet.getTaskName("generateResources", null)) + .getAbsolutePath(), + "--existing", project.project(":common").file("src/${sourceSet.name}/resources/").absolutePath, + "--existing", project.file("src/${sourceSet.name}/resources/").absolutePath, + ) + } + + register("data") { + configureForData("computercraft", sourceSets.main.get()) + loadedMods = listOf(computercraftDatagen.get()) + } + + fun RunModel.configureForGameTest() { + systemProperty( + "cctest.sources", + project.project(":common").file("src/testMod/resources/data/cctest").absolutePath, ) - mods.named("computercraft") { - source(sourceSets["datagen"]) - } + programArgument("--mixin.config=computercraft-gametest.mixins.json") + loadedMods.add(testMod) + + jvmArgument("-ea") } - fun RunConfig.configureForGameTest() { - val old = lazyTokens["minecraft_classpath"] - lazyToken("minecraft_classpath") { - // Add all files in testMinecraftLibrary to the classpath. - val allFiles = mutableSetOf() - - val oldVal = old?.get() - if (!oldVal.isNullOrEmpty()) allFiles.addAll(oldVal.split(File.pathSeparatorChar)) - - for (file in configurations["testMinecraftLibrary"].resolve()) allFiles.add(file.absolutePath) - - allFiles.joinToString(File.pathSeparator) - } - - property("cctest.sources", project(":common").file("src/testMod/resources/data/cctest").absolutePath) - - arg("--mixin.config=computercraft-gametest.mixins.json") - - mods.register("cctest") { - source(sourceSets["testMod"]) - source(sourceSets["testFixtures"]) - source(project(":core").sourceSets["testFixtures"]) - } - } - - val testClient by registering { - workingDirectory(file("run/testClient")) - parent(client.get()) + register("testClient") { + client() + gameDirectory = file("run/testClient") configureForGameTest() - property("cctest.tags", "client,common") + systemProperty("cctest.tags", "client,common") } - val gameTestServer by registering { - workingDirectory(file("run/testServer")) + register("gametest") { + type = "gameTestServer" configureForGameTest() - property("forge.logging.console.level", "info") - jvmArg("-ea") + systemProperty("forge.logging.console.level", "info") + systemProperty( + "cctest.gametest-report", + layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(), + ) + gameDirectory = file("run/gametest") + } + + register("exampleClient") { + client() + loadedMods.add(exampleMod.get()) + } + + register("exampleData") { + configureForData("examplemod", sourceSets.examples.get()) + loadedMods.add(exampleMod.get()) } } } configurations { - minecraftLibrary { extendsFrom(minecraftEmbed.get()) } + additionalRuntimeClasspath { extendsFrom(jarJar.get()) } - // Move minecraftLibrary/minecraftEmbed out of implementation, and into runtimeOnly. - implementation { setExtendsFrom(extendsFrom - setOf(minecraftLibrary.get(), minecraftEmbed.get())) } - runtimeOnly { extendsFrom(minecraftLibrary.get(), minecraftEmbed.get()) } - - val testMinecraftLibrary by registering { + val testAdditionalRuntimeClasspath by registering { isCanBeResolved = true isCanBeConsumed = false // Prevent ending up with multiple versions of libraries on the classpath. - shouldResolveConsistentlyWith(minecraftLibrary.get()) + shouldResolveConsistentlyWith(additionalRuntimeClasspath.get()) + } + + for (testConfig in listOf("testClientAdditionalRuntimeClasspath", "gametestAdditionalRuntimeClasspath")) { + named(testConfig) { extendsFrom(testAdditionalRuntimeClasspath.get()) } } } @@ -126,31 +143,22 @@ dependencies { annotationProcessorEverywhere(libs.autoService) clientCompileOnly(variantOf(libs.emi) { classifier("api") }) - libs.bundles.externalMods.forge.compile.get().map { compileOnly(fg.deobf(it)) } - libs.bundles.externalMods.forge.runtime.get().map { runtimeOnly(fg.deobf(it)) } - - // fg.debof only accepts a closure to configure the dependency, so doesn't work with Kotlin. We create and configure - // the dep first, and then pass it off to ForgeGradle. - (create(variantOf(libs.create.forge) { classifier("slim") }.get()) as ExternalModuleDependency) - .apply { isTransitive = false }.let { compileOnly(fg.deobf(it)) } + modCompileOnly(libs.bundles.externalMods.forge.compile) + modRuntimeOnly(libs.bundles.externalMods.forge.runtime) + modCompileOnly(variantOf(libs.create.forge) { classifier("slim") }) // Depend on our other projects. api(commonClasses(project(":forge-api"))) { cct.exclude(this) } clientApi(clientClasses(project(":forge-api"))) { cct.exclude(this) } implementation(project(":core")) { cct.exclude(this) } - minecraftEmbed(libs.cobalt) { - val version = libs.versions.cobalt.get() - jarJar.ranged(this, "[$version,${getNextVersion(version)})") - } - minecraftEmbed(libs.jzlib) { - jarJar.ranged(this, "[${libs.versions.jzlib.get()},)") - } + jarJar(libs.cobalt) + jarJar(libs.jzlib) // We don't jar-in-jar our additional netty dependencies (see the tasks.jarJar configuration), but still want them // on the legacy classpath. - minecraftLibrary(libs.netty.http) { isTransitive = false } - minecraftLibrary(libs.netty.socks) { isTransitive = false } - minecraftLibrary(libs.netty.proxy) { isTransitive = false } + additionalRuntimeClasspath(libs.netty.http) { isTransitive = false } + additionalRuntimeClasspath(libs.netty.socks) { isTransitive = false } + additionalRuntimeClasspath(libs.netty.proxy) { isTransitive = false } testFixturesApi(libs.bundles.test) testFixturesApi(libs.bundles.kotlin) @@ -163,8 +171,8 @@ dependencies { testModImplementation(testFixtures(project(":forge"))) // Ensure our test fixture dependencies are on the classpath - "testMinecraftLibrary"(libs.bundles.kotlin) - "testMinecraftLibrary"(libs.bundles.test) + "testAdditionalRuntimeClasspath"(libs.bundles.kotlin) + "testAdditionalRuntimeClasspath"(libs.bundles.test) testFixturesImplementation(testFixtures(project(":core"))) } @@ -181,23 +189,6 @@ tasks.processResources { } tasks.jar { - finalizedBy("reobfJar") - archiveClassifier.set("slim") - - for (source in cct.sourceDirectories.get()) { - if (source.classes && source.external) from(source.sourceSet.output) - } -} - -tasks.sourcesJar { - for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource) -} - -tasks.jarJar { - finalizedBy("reobfJarJar") - archiveClassifier.set("") - duplicatesStrategy = DuplicatesStrategy.FAIL - // Include all classes from other projects except core. val coreSources = project(":core").sourceSets["main"] for (source in cct.sourceDirectories.get()) { @@ -210,7 +201,9 @@ tasks.jarJar { } } -tasks.assemble { dependsOn("jarJar") } +tasks.sourcesJar { + for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource) +} // Check tasks @@ -218,22 +211,15 @@ tasks.test { systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath()) } -val runGametest by tasks.registering(JavaExec::class) { - group = LifecycleBasePlugin.VERIFICATION_GROUP - description = "Runs tests on a temporary Minecraft instance." - dependsOn("cleanRunGametest") +val runGametest = tasks.named("runGametest") { usesService(MinecraftRunnerService.get(gradle)) - - setRunConfig(minecraft.runs["gameTestServer"]) - - systemProperty("cctest.gametest-report", layout.buildDirectory.dir("test-results/$name.xml").getAbsolutePath()) } cct.jacoco(runGametest) tasks.check { dependsOn(runGametest) } val runGametestClient by tasks.registering(ClientJavaExec::class) { description = "Runs client-side gametests with no mods" - setRunConfig(minecraft.runs["testClient"]) + copyFromForge("runTestClient") tags("client") } cct.jacoco(runGametestClient) @@ -247,22 +233,12 @@ tasks.register("checkClient") { // Upload tasks modPublishing { - output.set(tasks.jarJar) -} - -// Don't publish the slim jar -for (cfg in listOf(configurations.apiElements, configurations.runtimeElements)) { - cfg.configure { artifacts.removeIf { it.classifier == "slim" } } + output.set(tasks.reobfJar) } publishing { publications { named("maven", MavenPublication::class) { - fg.component(this) - // jarJar.component is broken (https://github.com/MinecraftForge/ForgeGradle/issues/914), so declare the - // artifact explicitly. - artifact(tasks.jarJar) - mavenDependencies { cct.configureExcludes(this) exclude(libs.jei.forge.get()) 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..e54f0f6fb --- /dev/null +++ b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleMod.java @@ -0,0 +1,92 @@ +package com.example.examplemod; + +import com.example.examplemod.peripheral.BrewingStandPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.CapabilityToken; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.RegisterEvent; + +import javax.annotation.Nullable; +import java.util.function.Function; + +/** + * The main entry point for the Forge version of our example mod. + */ +@Mod(ExampleMod.MOD_ID) +public class ForgeExampleMod { + public ForgeExampleMod() { + // Register our turtle upgrade. If writing a Forge-only mod, you'd normally use DeferredRegister instead. + // However, this is an easy way to implement this in a multi-loader-compatible manner. + + // @start region=turtle_upgrades + var modBus = FMLJavaModLoadingContext.get().getModEventBus(); + modBus.addListener((RegisterEvent event) -> { + event.register(TurtleUpgradeSerialiser.registryId(), new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"), () -> ExampleMod.EXAMPLE_TURTLE_UPGRADE); + }); + // @end region=turtle_upgrades + + modBus.addListener((FMLCommonSetupEvent event) -> ExampleMod.registerComputerCraft()); + + MinecraftForge.EVENT_BUS.addGenericListener(BlockEntity.class, ForgeExampleMod::attachPeripherals); + } + + // @start region=peripherals + // The main function to attach peripherals to block entities. This should be added to the Forge event bus. + public static void attachPeripherals(AttachCapabilitiesEvent event) { + if (event.getObject() instanceof BrewingStandBlockEntity brewingStand) { + PeripheralProvider.attach(event, brewingStand, BrewingStandPeripheral::new); + } + } + + // Boilerplate for adding a new capability provider + + public static final Capability CAPABILITY_PERIPHERAL = CapabilityManager.get(new CapabilityToken<>() { + }); + private static final ResourceLocation PERIPHERAL = new ResourceLocation(ExampleMod.MOD_ID, "peripheral"); + + // A {@link ICapabilityProvider} that lazily creates an {@link IPeripheral} when required. + private static final class PeripheralProvider implements ICapabilityProvider { + private final O blockEntity; + private final Function factory; + private @Nullable LazyOptional peripheral; + + private PeripheralProvider(O blockEntity, Function factory) { + this.blockEntity = blockEntity; + this.factory = factory; + } + + private static void attach(AttachCapabilitiesEvent event, O blockEntity, Function factory) { + var provider = new PeripheralProvider<>(blockEntity, factory); + event.addCapability(PERIPHERAL, provider); + event.addListener(provider::invalidate); + } + + private void invalidate() { + if (peripheral != null) peripheral.invalidate(); + peripheral = null; + } + + @Override + public LazyOptional getCapability(Capability capability, @Nullable Direction direction) { + if (capability != CAPABILITY_PERIPHERAL) return LazyOptional.empty(); + if (blockEntity.isRemoved()) return LazyOptional.empty(); + + var peripheral = this.peripheral; + return (peripheral == null ? (this.peripheral = LazyOptional.of(() -> factory.apply(blockEntity))) : peripheral).cast(); + } + } + // @end region=peripherals +} 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..68c635669 --- /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.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +/** + * The client-side entry point for the Forge version of our example mod. + */ +@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) +public class ForgeExampleModClient { + // @start region=turtle_modellers + @SubscribeEvent + public static void onRegisterTurtleModellers(RegisterTurtleModellersEvent event) { + event.register(ExampleMod.EXAMPLE_TURTLE_UPGRADE, TurtleUpgradeModeller.flatItem()); + } + // @end region=turtle_modellers +} 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..7a6860dc8 --- /dev/null +++ b/projects/forge/src/examples/java/com/example/examplemod/ForgeExampleModDataGenerator.java @@ -0,0 +1,19 @@ +package com.example.examplemod; + +import com.example.examplemod.data.ExampleModDataGenerators; +import net.minecraftforge.data.event.GatherDataEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +/** + * The data generator entrypoint for the Forge version of our example mod. + * + * @see ExampleModDataGenerators The main implementation + */ +@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD) +public class ForgeExampleModDataGenerator { + @SubscribeEvent + public static void gather(GatherDataEvent event) { + ExampleModDataGenerators.run(event.getGenerator().getVanillaPack(true)); + } +} diff --git a/projects/forge/src/examples/resources/META-INF/mods.toml b/projects/forge/src/examples/resources/META-INF/mods.toml new file mode 100644 index 000000000..bf2ca5aff --- /dev/null +++ b/projects/forge/src/examples/resources/META-INF/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/lints/src/main/kotlin/cc/tweaked/javadoc/SnippetTaglet.kt b/projects/lints/src/main/kotlin/cc/tweaked/javadoc/SnippetTaglet.kt new file mode 100644 index 000000000..ac6a2c362 --- /dev/null +++ b/projects/lints/src/main/kotlin/cc/tweaked/javadoc/SnippetTaglet.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.javadoc + +import com.sun.source.doctree.DocTree +import com.sun.source.doctree.TextTree +import com.sun.source.doctree.UnknownInlineTagTree +import com.sun.source.util.DocTreePath +import jdk.javadoc.doclet.* +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import java.util.regex.Pattern +import javax.lang.model.element.Element +import javax.tools.Diagnostic +import kotlin.io.path.extension + +/** + * A primitive reimplementation of Java 21's `@snippet` tag. This only supports including external snippets via `file` + * and `class`, and not the inline body. + */ +class SnippetTaglet : Taglet { + override fun getName(): String = "snippet" + override fun isInlineTag(): Boolean = true + override fun getAllowedLocations(): Set = locations + + private lateinit var env: DocletEnvironment + private lateinit var reporter: Reporter + private lateinit var snippetPath: List + + override fun init(env: DocletEnvironment, doclet: Doclet) { + super.init(env, doclet) + this.env = env + reporter = (doclet as StandardDoclet).reporter + + this.snippetPath = + System.getProperty("cc.snippet-path")?.split(File.pathSeparatorChar)?.map { File(it) } ?: emptyList() + } + + /** Parse our attributes into a key/value map */ + private fun parseAttributes(contents: String): Map { + val attributes = mutableMapOf() + val attributeMatcher = attribute.matcher(contents) + var lastIndex = 0 + while (attributeMatcher.find()) { + val key = attributeMatcher.group(1) + val value = attributeMatcher.group(2) + if (attributes.contains(key)) throw SnippetException("Duplicate attribute '$key'") + attributes[key] = value + lastIndex = attributeMatcher.end() + } + + while (lastIndex < contents.length) { + val c = contents[lastIndex] + if (c != ' ') throw SnippetException("Unexpected '$c'") + } + + return attributes + } + + /** Locate our snippet file within the [snippetPath] */ + private fun findSnippetFile(fileName: String): Path = snippetPath.firstNotNullOfOrNull { + val found = it.resolve(fileName) + if (found.exists()) found.toPath() else null + } ?: throw SnippetException("Cannot find file '$fileName'") + + private fun processInlineTag(tag: UnknownInlineTagTree): String { + val tagContent = tag.content + if (tagContent.size != 1 || tagContent[0].kind != DocTree.Kind.TEXT) throw SnippetException("Expected a single text node") + val attributes = parseAttributes((tagContent[0] as TextTree).body) + + val hasFile = attributes.contains("file") + val hasClass = attributes.contains("class") + if (hasFile && hasClass) throw SnippetException("Cannot specify file and class") + + val file = when { + hasFile -> findSnippetFile(attributes["file"]!!) + hasClass -> findSnippetFile(attributes["class"]!!.replace('.', '/') + ".java") + else -> throw SnippetException("Snippet has no contents (must have file or class)") + } + + // And generate our snippet + var snippetContents = Files.readString(file) + + val region = attributes["region"] + if (region != null) { + val matcher = + Pattern.compile("// @start region=" + Pattern.quote(region) + "\n(.*)\\s*// @end region=" + Pattern.quote(region), Pattern.DOTALL) + .matcher(snippetContents) + if (!matcher.find()) throw SnippetException("Cannot find region '$region'") + snippetContents = matcher.group(1).trimIndent() + } + + return makeSnippet(file.extension, snippetContents) + } + + override fun toString(tags: List, element: Element): String { + if (tags.size != 1) throw IllegalArgumentException("Tags should be length 1") + val tag = tags[0] as UnknownInlineTagTree + + try { + return processInlineTag(tag) + } catch (e: SnippetException) { + reporter.print( + Diagnostic.Kind.ERROR, + DocTreePath.getPath(env.docTrees.getPath(element), env.docTrees.getDocCommentTree(element), tag), + "Invalid @snippet. ${e.message}", + ) + return "@snippet" + } + } + + companion object { + private val locations = EnumSet.allOf(Taglet.Location::class.java) + private val attribute = Pattern.compile(" *([a-z]+) *= *([^ ]+)") + + /** Escape our snippet HTML and wrap it into a code block */ + private fun makeSnippet(extension: String, contents: String): String { + val out = StringBuilder(contents.length + 60) + out.append("
")
+            for (element in contents) {
+                when (element) {
+                    '<' -> out.append("<")
+                    '>' -> out.append(">")
+                    '&' -> out.append("&")
+                    else -> out.append(element)
+                }
+            }
+            out.append("
") + return out.toString() + } + } +} + +private class SnippetException(message: String) : Exception(message) 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 cee5ae6c8..6e91eff3f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,25 +8,10 @@ pluginManagement { mavenCentral() gradlePluginPortal() - maven("https://maven.minecraftforge.net") { - name = "Forge" + maven("https://maven.neoforged.net") { + name = "NeoForge" content { - includeGroup("net.minecraftforge") - includeGroup("net.minecraftforge.gradle") - } - } - - maven("https://maven.parchmentmc.org") { - name = "Librarian" - content { - includeGroupByRegex("^org\\.parchmentmc.*") - } - } - - maven("https://repo.spongepowered.org/repository/maven-public/") { - name = "Sponge" - content { - includeGroup("org.spongepowered") + includeGroup("net.neoforged") } } @@ -45,14 +30,6 @@ pluginManagement { } } } - - resolutionStrategy { - eachPlugin { - if (requested.id.id == "org.spongepowered.mixin") { - useModule("org.spongepowered:mixingradle:${requested.version}") - } - } - } } val mcVersion: String by settings