Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Java 21
uses: actions/setup-java@v4
Expand All @@ -32,4 +35,40 @@ jobs:
uses: gradle/actions/setup-gradle@v4

- name: Build all Stonecutter nodes
run: ./gradlew build --no-daemon
run: ./gradlew cleanTest build generatePomFileForMavenPublication generateMetadataFileForMavenPublication --no-daemon

- name: Verify publication contracts
run: python3 scripts/check-publications.py

- name: Run server GameTests on all loaders and versions
run: >-
timeout --foreground 600s ./gradlew
:testiarium:forge:1.20.1:runGameTestServer
:testiarium:forge:1.21.1:runGameTestServer
:testiarium:fabric:1.20.1:runServer
:testiarium:fabric:1.21.1:runServer
:tweakium:forge:1.20.1:runGameTestServer
:tweakium:forge:1.21.1:runGameTestServer
:tweakium:fabric:1.20.1:runPeripheralGameTest
:tweakium:fabric:1.21.1:runPeripheralGameTest
--no-daemon

- name: Build with 1.21.1 active
run: |
./gradlew 'Set active project to 1.21.1' --no-daemon
./gradlew build generatePomFileForMavenPublication generateMetadataFileForMavenPublication --no-daemon
python3 scripts/check-publications.py

- name: Verify switching back preserves tracked sources
run: |
./gradlew 'Set active project to 1.20.1' --no-daemon
git diff --exit-code

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
projects/**/build/test-results/**
projects/**/build/reports/tests/**
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# SirEdvin's Minecraft modding libs

This readme under construction
Minecraft libraries built for 1.20.1 (Java 17, Fabric/Forge) and 1.21.1
(Java 21, Fabric/NeoForge) with Stonecutter. The historical `forge` project
paths and Maven artifact names also cover NeoForge on 1.21.1.

## Source ownership

- Commit with 1.20.1 active; it is the canonical VCS state.
- Shared sources live in `projects/<library>/src` and its loader branches.
- `versions/<version>/src` contains intentional version-specific overrides.
Those files take precedence, including when that version is active.
- Do not edit `versions/<version>/build/generated/stonecutter` or other build outputs.
- The libraries currently use separate trees. Run switching commands from the
repository root so all four trees switch together.

## Verification

```sh
./gradlew cleanTest build generatePomFileForMavenPublication generateMetadataFileForMavenPublication --no-daemon
python3 scripts/check-publications.py
./gradlew 'Set active project to 1.21.1' --no-daemon
./gradlew build generatePomFileForMavenPublication generateMetadataFileForMavenPublication --no-daemon
python3 scripts/check-publications.py
./gradlew 'Set active project to 1.20.1' --no-daemon
```

The publication check inspects all 24 main publications, including dependencies,
feature capabilities, source JARs, loader metadata and Java bytecode targets.
CI also runs server GameTests on all four version/loader combinations and checks
that switching back leaves tracked files unchanged. Formatting is checked once
from the repository root, ratcheted against `origin/multiversion`.

Use scratch log files for verbose local Gradle output (`> "$TMPDIR/build.log" 2>&1`).
The server matrix includes Testiarium and Tweakium's peripheral tests. The report
guard removes old XML before launch and requires a fresh, nonempty report with no
failures/errors; Minecraft startup can fail while the process still exits zero.

Stonecutter is pinned to stable 0.9.8. The newer 0.10-beta.1 is not adopted: it
requires a Java 25 Gradle launcher, Gradle 9.7+, and a separate preprocessing/API
migration. See [the migration audit](docs/stonecutter-migration-audit.md) for
gameplay/API differences, inherited bugs, fixes, and verification scope.
80 changes: 78 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication

Expand All @@ -7,6 +8,7 @@ plugins {
id("net.neoforged.moddev.legacyforge") version "2.0.142" apply false
id("site.siredvin.root") version "0.9.0"
id("site.siredvin.release") version "0.9.0"
id("site.siredvin.linting") version "0.9.0"
id("com.github.ben-manes.versions") version "0.51.0"
}

Expand All @@ -20,6 +22,8 @@ val setupSubproject = subprojectShaking::setupSubproject
val subprojectConfig = subprojectShaking
val projectGroup: String by project

apply(from = "gradle/gametest-reports.gradle.kts")

subprojects {
if (projectDir.parentFile.name == "versions") {
extra["minecraftVersion"] = name
Expand All @@ -28,14 +32,62 @@ subprojects {
if (name == "1.20.1") JavaVersion.VERSION_17 else JavaVersion.VERSION_21,
)
setupSubproject(this)
// ponytail: Spotless rejects Stonecutter's shared source directories outside leaf projects.
tasks.matching { it.name.startsWith("spotless") }.configureEach { enabled = false }
// Shared sources are checked once, from the repository root that owns them.
extensions.configure<SpotlessExtension> {
java { target(emptyList<String>()) }
kotlin { target(emptyList<String>()) }
kotlinGradle { target(emptyList<String>()) }
}
if (!path.contains(":fabric:")) {
pluginManager.apply("net.fabricmc.fabric-loom-companion")
}
}

val minecraftVersion = name
// Gradle identifies projects by group/name/version; every leaf has the same version name.
// Keep internal module identities unique while publishing under projectGroup below.
group = "$projectGroup.${path.substringBeforeLast(':').removePrefix(":").replace(':', '.')}"
// Stonecutter excludes overrides from generated sources, but not the active shared tree.
val sharedSources = projectDir.parentFile.parentFile.resolve("src").toPath()
val localSources = projectDir.resolve("src")
val overridden = Spec<FileTreeElement> { entry ->
val source = entry.file.toPath()
source.startsWith(sharedSources) && localSources.resolve(sharedSources.relativize(source).toString()).isFile
}
pluginManager.withPlugin("java") {
extensions.getByType<SourceSetContainer>().configureEach {
java.exclude(overridden)
resources.exclude(overridden)
allJava.exclude(overridden)
allSource.exclude(overridden)
}
}
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
extensions.getByType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>().sourceSets.configureEach {
kotlin.exclude(overridden)
}
}
afterEvaluate {
// Enable after Kotlin: enabling earlier publishes a competing kotlinSourcesJar.
extensions.configure<JavaPluginExtension> { withSourcesJar() }
}
if (path.contains(":fabric:") || path.contains(":forge:")) {
val commonProject = project(":${path.split(':')[1]}:$minecraftVersion")
afterEvaluate {
tasks.named<Jar>("sourcesJar") {
from(commonProject.extensions.getByType<SourceSetContainer>().named("main").map { it.allSource })
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
if (path.contains(":forge:")) {
extensions.getByType<net.neoforged.moddevgradle.dsl.ModDevExtension>().setAccessTransformers(
extensions.getByType<SourceSetContainer>().named("main").map {
it.resources.matching { include("META-INF/accesstransformer.cfg") }
},
)
tasks.named("createMinecraftArtifacts") { dependsOn("stonecutterGenerate") }
}
}
}
pluginManager.withPlugin("maven-publish") {
extensions.configure<PublishingExtension> {
publications.withType<MavenPublication>().configureEach {
Expand Down Expand Up @@ -74,3 +126,27 @@ subprojects {
repositories {
mavenCentral()
}

spotless {
// Enforce formatting on changed files without rewriting the inherited version snapshots.
ratchetFrom("origin/multiversion")
java {
target(
fileTree("projects") {
include("**/src/**/*.java")
exclude("**/build/**", "**/src/generated/**")
},
)
}
kotlin {
target(
fileTree("projects") {
include("**/src/**/*.kt")
exclude("**/build/**", "**/src/generated/**")
},
)
}
kotlinGradle {
target("*.gradle.kts", "projects/*/*.gradle.kts", "projects/*/*/*.gradle.kts")
}
}
67 changes: 67 additions & 0 deletions docs/stonecutter-migration-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Stonecutter migration audit

Reviewed 2026-10-16. Baselines are local historical branch tips `1.20` (`54a9c1dfa2e1db4ebaeb6f53dfce3f57cb3d005e`) and `1.21` (`da613f40a35bd2eb5a490aff9bc5860fe776a3bf`), compared with migration-contract commit `47a80ca9349e009cf2e21b517b22b5ee99b739b2` and the fixes described below.

## Scope and conclusion

Mapped all tracked source/resource paths from the historical core/Fabric/Forge modules into each Stonecutter branch, applying version-local full-file override precedence. This is a migration-fidelity review, not a claim that all existing gameplay or third-party integrations are bug-free.

Before this audit's changes:

| Target | Identical files | Changed | Added | Removed |
| --- | ---: | ---: | ---: | ---: |
| 1.20.1 | 417 | 9 | 0 | 1 |
| 1.21.1 | 433 | 10 | 2 | 0 |

On 1.20.1, production `main` sources/resources are unchanged except for a final newline in the access widener: 327 byte-identical files and that one formatting-only change. Remaining changes affect tests; the removed file is the obsolete Fabric test bootstrap, not gameplay. No production files were removed on either version.

On 1.21.1, 337 production `main` files are identical and eight differ. The migration is not behavior-neutral: the following differences matter to consumers.

## Significant pre-existing migration differences

1. **Unsided NeoForge storage lookup.** `projects/broccolium/forge/versions/1.21.1/src/main/kotlin/site/siredvin/broccolium/modules/storage/ForgeStorageUtils.kt` now passes a nullable direction through for block item/fluid/energy and entity fluid/energy capabilities, instead of replacing `null` with `Direction.UP`. A provider can expose different content or access rules for unsided versus top-side access. Callers that require the old top-side behavior should pass `UP` explicitly. Retained the migration's native nullable-context behavior.
2. **Fake-player equipment attributes.** The four 1.21.1 `FakePlayerProvider*` implementations use `ItemStack.forEachModifier(EquipmentSlot.MAINHAND)` instead of adding every entry in the attribute-modifier component. This respects main-hand slot applicability and the native stack iteration path. Combat/tool attribute results can differ for slot-specific modifiers; the old implementation could incorrectly apply armor/offhand modifiers. Retained the scoped behavior.
3. **Enchantment representation API.** `LuaRepresentation.forEnchantment` now accepts `Holder<Enchantment>`; its `name` becomes the registered identifier, while `displayName` uses the full level-qualified name. `forEnchantments` likewise expects holder keys. This changes Lua output and the public Kotlin/Java contract (including the single-enchantment JVM signature); downstream callers compiled against the old API must be updated/recompiled. Retained the registry-aware API, but it should be called out in release notes.
4. **Component-patch persistence and encoding errors.** `CompoundTagDataStorage.patch` now notifies persistence after a successful write/clear, and `DataComponentUtil.patchToNBT` throws on encoding failure rather than silently returning no encoded value. Existing tests verify successful notification and that failed encoding preserves the stored tag without notification. This is deliberate correctness behavior, but callers can now observe an encoding exception.

The 1.21.1 Testiarium CCT mixin configuration is also newly connected. This affects the optional test harness rather than shipped gameplay. No recipe, texture, registration, or Peripheralium production-source divergence was found in the path-normalized comparison.

## Confirmed bugs fixed by this audit

### High: upgrade writes lost component-removal entries

Both pocket and turtle custom-data setters copied only present patch values. A data-component patch also uses empty optionals to mean an explicit removal; discarding those entries could restore a removed default component when the upgrade is reconstructed. The bug already existed on historical `1.21`.

Fixed both setters in `projects/tweakium/versions/1.21.1/src/main/kotlin/site/siredvin/tweakium/modules/peripheral/util/DataStorageUtil.kt` to copy removal entries with `builder.remove`. Added a parameterized test exercising the real adapters through narrow CC API proxies, including repeated writes, key removal, and mutation. Both cases failed before the fix and passed afterward. This is adapter regression coverage, not a live computer save/reload test.

### High: NeoForge entity inventory lookup threw before querying

`null as Void` is a non-null Kotlin cast and throws before the entity item capability lookup. This also existed on historical `1.21`. Pass plain nullable `null` instead. A loader-run GameTest verifies a chest minecart's native item capability and its diamond contents; it failed before the fix and passed afterward.

### Medium: misleading or broken peripheral GameTest wiring

The new entity regression initially revealed incomplete test execution rather than just the gameplay failure: NeoForge lacked Broccolium in the loaded-mod set; the fixture was missing; and the 1.20.1 Forge testmod's compile classpath lacked Testiarium's common main output. Fixed these test-only inputs and retained test-only isolation.

Added `gradle/gametest-reports.gradle.kts`: remove the exact previous XML before launch, require a new report with at least one non-skipped case, reject failure/error elements, and check declared counts. Some startup failures exit zero, so a successful Gradle process alone is insufficient. CI now runs Tweakium peripheral tests alongside the existing Testiarium matrix.

## Stonecutter and companion toolkit

- Upgraded Minecraft-Modding-Libs and TemplateProject from **0.9.7 to stable 0.9.8**. The stable release adds IDE parameter annotations and updated KDoc links, not gameplay transformations or fixes for consumer-specific build contracts.
- Inspected **0.10-beta.1**, the latest prerelease in the checked snapshot metadata. Did not adopt it: it requires a Java 25 Gradle launcher and Gradle 9.7+, and changes preprocessing, property accessors, flags, task APIs and model schemas. Alpha.8 restored structured-property exposure removed earlier in the prerelease series; the removal is not current beta behavior.
- TemplateProject's legacy Forge artifact-generation task now depends on `stonecutterGenerate`, matching its existing NeoForge ordering.
- modding-buildenv candidate **0.9.3** tracks the complete metadata expansion map and shared build script for Fabric/Forge/NeoForge resource processing. Built and published it only to a scratch file Maven repository, then consumed it through an uncommitted init script. Changing the raw Broccolium dependency property regenerated all four Tweakium loader/version metadata outputs without `clean` or `--rerun-tasks`; their contents were inspected and the normal values restored.
- A Gradle 9 composite build of the toolkit fails in legacy ForgeGradle accessor generation. Its own Gradle 8 wrapper plus a file-Maven consumer works; this audit does not upgrade that wrapper or publish the candidate remotely. Production consumers retain published toolkit pins until a separate release.
- Updated the Stonecutter skill and its release/migration-contract reference in `minecraft-modding-skills`, plus local Testiarium guidance on fresh-report validation and source-set/loaded-mod wiring.

Upstream evidence: Plugin Portal plus release/snapshot Maven metadata; `stonecutter/docs` commit `2f685fdf78a83c5453e007177b5e4dc4f14b29e6` and stable source commit `73632f3266a1e81683dedb4a52168ec53c583442`, compared with published source JARs. Stable metadata reports 0.9.8 (2026-08-31); snapshot metadata reports 0.10-beta.1 (2026-10-14).

## Verification and limits

- Root build, fresh JUnit execution, publication generation and verification with 1.20.1 active: **1,598 tests, zero failures/errors/skips; 24 publications verified**.
- Server matrix: Testiarium and Tweakium on Forge/Fabric 1.20.1 and NeoForge/Fabric 1.21.1: **eight reports, 21 cases, zero failures/errors, eight expected optional skips**. Skips are not counted as passes.
- TemplateProject: full build with its canonical `1.21.1-fabric` node active, switch to `1.20.1-forge`, another full build, and reset.
- modding-buildenv: own-wrapper `check`, local publication, and incremental consumer metadata verification on all four nodes. Fabric/NeoForge exercise the candidate conventions; the consumer's 1.20.1 Forge node uses its existing ModDevGradle resource wiring. The toolkit's legacy ForgeGradle convention was compiled by `check`, not separately runtime-tested in a ForgeGradle consumer.
- Repeated the root build, fresh JUnit run and publication verification with **1.21.1 active**: again **1,598 tests with zero failures/errors/skips, and 24 verified publications**. Restored all four controllers to 1.20.1; no switch-only source/controller changes remain. TemplateProject is likewise reset without source churn.
- The skill repository validator passed using an isolated `uv run --with pyyaml --with jsonschema` environment; `git diff --check` passed in all four repositories.

All Gradle/Minecraft runs use `--no-daemon`, with stdout/stderr redirected to scratch logs and explicit deadlines for runtime tests. The audit did not perform interactive client/rendering checks, world save/reload coverage, exhaustive fake-player combat scenarios, live upgrade reinstallation, a complete CCT/client runtime matrix, or downstream binary-compatibility tests. These limits should not be inferred from compilation or generic server smoke results.
Loading
Loading