Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b03c910
Revert "Remove onboarding screen"
NonSwag Jul 27, 2026
49535cb
_proper?_ scaling
NonSwag Jul 27, 2026
2357368
use pointing hand when hovering clickable text
NonSwag Jul 27, 2026
3ee0b42
Rework screen definitions
NonSwag Aug 2, 2026
82dfe54
Rework screen definitions
NonSwag Aug 2, 2026
49b49c8
Java 17 support
LuggaPugga Aug 15, 2026
fe33ab7
Add client consent screen to telemetry screen and show on initial launch
NonSwag Aug 15, 2026
db2d16f
Refactor config module to better accommodate client consent screen
NonSwag Aug 15, 2026
eeff0e5
port consent screen to old versions
NonSwag Aug 19, 2026
cca78d1
Simplify gradle setup; use JPMS
NonSwag Sep 5, 2026
750f113
Cleanup and fixes
NonSwag Sep 5, 2026
cd2a846
no idea
NonSwag Sep 29, 2026
73f1575
temp fix
NonSwag Sep 29, 2026
f3a2f8a
Remove example
NonSwag Oct 13, 2026
5d61c04
Make consent screen opt-in
NonSwag Oct 13, 2026
29b575e
Rename onboarding screen to consent screen
NonSwag Oct 13, 2026
a95604e
Cleanup shadow mess
NonSwag Oct 13, 2026
ebb1000
Declare plugins in version catalog
NonSwag Oct 13, 2026
69badcb
Remove unused kotlin dependency
NonSwag Oct 13, 2026
8483a06
Move to version catalog
NonSwag Oct 13, 2026
0a69fa2
NeoForge 26.3 support
NonSwag Oct 13, 2026
89e5ac0
pre1
NonSwag Oct 13, 2026
beb3c30
Fix screen closing
NonSwag Oct 13, 2026
bde21ba
Provide source and javadocs jars
NonSwag Oct 13, 2026
465b285
fix layout
NonSwag Oct 13, 2026
5af3942
code cleanup (if you can call it that)
NonSwag Oct 13, 2026
1cb69df
icon not logo
NonSwag Oct 13, 2026
7507ef5
only one text box
NonSwag Oct 13, 2026
73c394d
add proper translations
NonSwag Oct 13, 2026
3ab9d57
Fix incorrect module version
NonSwag Oct 13, 2026
1be6690
move away from abstraction hell i guess
NonSwag Oct 13, 2026
c6f3450
cleanup package structure
NonSwag Oct 14, 2026
2ef49fb
cleanup i guess
NonSwag Oct 14, 2026
aae355e
configuration cache support
NonSwag Oct 14, 2026
9ea3548
add CONTRIBUTING.md
NonSwag Oct 14, 2026
41fcdda
fix some stuff
NonSwag Oct 14, 2026
b13d800
prevent parallel publishing to avoid corruption
NonSwag Oct 14, 2026
3f29bc8
fix clickable text offset
NonSwag Oct 14, 2026
30ad493
[ci skip] remove leftover todo
NonSwag Oct 14, 2026
47b6c33
only persist consent screen config entry on the client
NonSwag Oct 14, 2026
6ea3c3d
[ci skip] pre2
NonSwag Oct 14, 2026
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
27 changes: 0 additions & 27 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,3 @@ jobs:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Build with Gradle
run: ./gradlew build
- name: Test with Gradle
run: ./gradlew test
platform-compat:
name: Check ${{ matrix.platform }} compatibility modules
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- platform: Fabric
task: checkFabricPlatformCompat
- platform: NeoForge
task: checkNeoForgePlatformCompat
steps:
- name: Checkout sources
uses: actions/checkout@v7.0.1
- name: Setup Java
uses: actions/setup-java@v6
with:
distribution: 'temurin'
java-version: 25
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Check compatibility modules
run: ./gradlew ${{ matrix.task }}
2 changes: 1 addition & 1 deletion .github/workflows/maven-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
env:
REPOSITORY_USER: ${{ secrets.REPOSITORY_USER }}
REPOSITORY_TOKEN: ${{ secrets.REPOSITORY_TOKEN }}
run: ./gradlew publish publishPlatformCompat
run: ./gradlew publish
102 changes: 102 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Contributing

## Building

Run Gradle from the repository root:

```sh
./gradlew build
```

This compiles, tests, and packages every module, including the Fabric and NeoForge artifacts for every supported
Minecraft version. To work on a single version, build only its project:

```sh
./gradlew :fabric:versions:1.21.10:build
./gradlew :neoforge:versions:1.21.10:build
```

## Minecraft versions

Fabric and NeoForge are built and published once per Minecraft version, as
`dev.faststats.metrics:<fabric|neoforge>:<sdk-version>+mc<minecraft-version>`. Each artifact only declares the
Minecraft version it was built for.

### Where the code lives

| Directory | Contents |
|---------------------------------|--------------------------------------------------------------|
| `fabric/`, `neoforge/` | Platform code that is the same for every Minecraft version |
| `onboarding/` | Onboarding code without any Minecraft dependency |
| `fabric/versions/<version>` | Fabric code that depends on the Minecraft version |
| `neoforge/versions/<version>` | NeoForge code that depends on the Minecraft version |
| `onboarding/versions/<version>` | The consent screen and its mixins, compiled into both of them |

The directories in `<platform>/versions` are called variants. A variant is named after the first Minecraft version
that needs its code, and every later version is built from it until the next variant. For example, Fabric 1.21.10 is
built from `fabric/versions/1.21.9` and `onboarding/versions/1.21.9`.

Each Minecraft version still gets its own Gradle project, `:<platform>:versions:<version>`, which compiles the variants
against exactly that version. Their directories in `<platform>/versions` are created by Gradle and only contain build
output.

The onboarding sources are compiled into the Fabric and NeoForge projects rather than built on their own. Files that
are identical between onboarding variants only exist in the oldest variant using them. The other variants reference
them through the `sharedSources` table in `build-logic/src/main/kotlin/faststats.onboarding-sources.gradle.kts`.

### Naming

- **Variant directories:** the Minecraft version, e.g. `1.21.9` or `26.1`.
- **Fabric and NeoForge packages:** the variant with underscores, e.g. `dev.faststats.fabric.compat.v1_21_9`. Each
variant registers its `CompatibilityLayerImpl` in
`src/main/resources/META-INF/services/dev.faststats.<platform>.compat.CompatibilityLayer`.
- **Onboarding packages:** the same in every variant, `dev.faststats.screen.minecraft` and
`dev.faststats.screen.minecraft.mixin`, so files can be shared between variants.
- **Version catalog entries:** the Minecraft version with `x` instead of dots, e.g. `minecraft-v1x21x10`,
`fabric-api-v1x21x10` and `neoforge-v1x21x10`.

## Adding a Minecraft version

1. Add the version to `gradle/libs.versions.toml` (the release numbers below are placeholders):

```toml
[versions]
neoforge-v26x4 = "26.4.0.1"

[libraries]
minecraft-v26x4 = "com.mojang:minecraft:26.4"
fabric-api-v26x4 = "net.fabricmc.fabric-api:fabric-api:0.160.0+26.4"
```

2. Add it to `minecraftVersions` in `build-logic/src/main/kotlin/MinecraftVersions.kt`, after the newest version:

```kotlin
MinecraftVersion("26.4", java = 25, obfuscated = false),
```

- `java`: the Java version the game requires. The code is compiled for it.
- `obfuscated`: whether the game is obfuscated and has to be remapped. Versions before 26.1 are.
- `fabricApi`: only needed when there is no Fabric API release for exactly this version, e.g.
`fabricApi = "26.1.2"`.

3. Build the new version:

```sh
./gradlew :fabric:versions:26.4:build :neoforge:versions:26.4:build
```

If it builds, the version is done. It is published with the next release.

### When the code needs changes

If a version doesn't compile, or needs different behavior, give it its own variant on the platform that changed:

1. Copy the closest older variant, e.g. `fabric/versions/26.1` to `fabric/versions/26.4`.
2. For Fabric and NeoForge, rename the package to the new variant, e.g. `v26_1` to `v26_4`, and update the class name
in its `META-INF/services` file.
3. For onboarding, the new variant only needs the files that change. For every file it takes unchanged from an older
variant, add it to that file's `sharedSources` entry, or add an entry if the file doesn't have one yet.
4. Fix the code and build every version from the new one onwards, as they now use the new variant too.

A variant only replaces the code of its own platform. A new Fabric variant doesn't affect NeoForge or onboarding, and
the other way around.
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ Use `build` for the reusable FastStats libraries:
```

Library jars are written to each module's `build/libs` directory. Fabric and NeoForge compatibility artifacts are
published under stable Maven artifact IDs with Minecraft range suffixes:
published under stable Maven artifact IDs for every supported Minecraft version:

```text
dev.faststats.metrics:fabric:<sdk-version>+mc26.1-26.2
dev.faststats.metrics:neoforge:<sdk-version>+mc26.1-26.2
dev.faststats.metrics:fabric:<sdk-version>+mc26.1.2
dev.faststats.metrics:neoforge:<sdk-version>+mc26.1.2
```

Use `checkPlatformCompat` to compile all Fabric and NeoForge compatibility modules.
Each artifact only declares the Minecraft version it was built for in its `fabric.mod.json` or `neoforge.mods.toml`.

See [CONTRIBUTING.md](CONTRIBUTING.md) for how the version-specific code is organized and how to add a Minecraft
version.

### Building everything

Expand All @@ -40,10 +43,3 @@ To compile and test all modules with the standard lifecycle, run:
./gradlew build
```

### Platform compatibility checks

Compile all published platform compatibility modules with:

```sh
./gradlew checkPlatformCompat
```
16 changes: 16 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
plugins {
`kotlin-dsl`
}

repositories {
gradlePluginPortal()
maven("https://maven.fabricmc.net/")
maven("https://maven.neoforged.net/releases")
}

fun plugin(plugin: Provider<PluginDependency>) = plugin.map {
"${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}"
}

dependencies {
implementation(plugin(libs.plugins.fabric.loom))
implementation(plugin(libs.plugins.moddev))
implementation(plugin(libs.plugins.shadow))
}
5 changes: 5 additions & 0 deletions build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ dependencyResolutionManagement {
repositories {
gradlePluginPortal()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
101 changes: 101 additions & 0 deletions build-logic/src/main/kotlin/MinecraftVersions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import org.gradle.api.Project
import java.io.File

/**
* A Minecraft version the mod platforms are compiled and published for.
*
* @param java the Java version the game runs on
* @param obfuscated whether the game is obfuscated and the jars have to be remapped
* @param fabricApi the Fabric API release to compile against
*/
class MinecraftVersion(
val name: String,
val java: Int,
val obfuscated: Boolean = true,
fabricApi: String = name,
) {
// the version catalog names its entries like minecraft-v1x21x10
val catalogName = catalogName(name)
val fabricApiCatalogName = catalogName(fabricApi)

override fun toString() = name
}

private fun catalogName(version: String) = "v" + version.replace('.', 'x')

// every supported Minecraft version, oldest first
// a version without its own directory in <platform>/versions is built from the closest older one with sources
val minecraftVersions = listOf(
MinecraftVersion("1.16.1", java = 17),
MinecraftVersion("1.16.2", java = 17),
MinecraftVersion("1.16.3", java = 17),
MinecraftVersion("1.16.4", java = 17),
MinecraftVersion("1.16.5", java = 17),
MinecraftVersion("1.17", java = 17),
MinecraftVersion("1.17.1", java = 17),
MinecraftVersion("1.18", java = 17),
MinecraftVersion("1.18.1", java = 17),
MinecraftVersion("1.18.2", java = 17),
MinecraftVersion("1.19", java = 17),
MinecraftVersion("1.19.1", java = 17),
MinecraftVersion("1.19.2", java = 17),
MinecraftVersion("1.19.3", java = 17),
MinecraftVersion("1.19.4", java = 17),
MinecraftVersion("1.20", java = 17),
MinecraftVersion("1.20.1", java = 17),
MinecraftVersion("1.20.2", java = 17),
MinecraftVersion("1.20.3", java = 17),
MinecraftVersion("1.20.4", java = 17),
MinecraftVersion("1.20.5", java = 21),
MinecraftVersion("1.20.6", java = 21),
MinecraftVersion("1.21", java = 21),
MinecraftVersion("1.21.1", java = 21),
MinecraftVersion("1.21.2", java = 21),
MinecraftVersion("1.21.3", java = 21),
MinecraftVersion("1.21.4", java = 21),
MinecraftVersion("1.21.5", java = 21),
MinecraftVersion("1.21.6", java = 21),
MinecraftVersion("1.21.7", java = 21),
MinecraftVersion("1.21.8", java = 21),
MinecraftVersion("1.21.9", java = 21),
MinecraftVersion("1.21.10", java = 21),
MinecraftVersion("1.21.11", java = 21),
MinecraftVersion("26.1", java = 25, obfuscated = false, fabricApi = "26.1.2"),
MinecraftVersion("26.1.1", java = 25, obfuscated = false, fabricApi = "26.1.2"),
MinecraftVersion("26.1.2", java = 25, obfuscated = false),
MinecraftVersion("26.2", java = 25, obfuscated = false),
MinecraftVersion("26.3", java = 25, obfuscated = false),
)

private val platformVersions = mapOf(
"fabric" to minecraftVersions,
"neoforge" to minecraftVersions.dropWhile { it.name != "1.20.6" },
"onboarding" to minecraftVersions,
)

// the platforms with a project per Minecraft version, the onboarding sources are compiled into them
val minecraftPlatforms = setOf("fabric", "neoforge")

fun minecraftVersions(platform: String): List<MinecraftVersion> = platformVersions.getValue(platform)

// projects are named :<platform>:versions:<minecraft version>
val Project.minecraftPlatform: String get() = path.split(':')[1]

val Project.minecraftVersion: MinecraftVersion
get() = minecraftVersions(minecraftPlatform).single { it.name == name }

/**
* The version whose sources [version] is built from, the closest version up to it with its own directory.
*/
fun Project.minecraftVariant(
platform: String = minecraftPlatform,
version: MinecraftVersion = minecraftVersion,
): MinecraftVersion = minecraftVersions(platform)
.takeWhile { it.name != version.name }
.plus(version)
.last { variantDirectory(platform, it).resolve("src").isDirectory }

fun Project.variantDirectory(
platform: String = minecraftPlatform,
variant: MinecraftVersion = minecraftVariant(platform),
): File = rootProject.file("$platform/versions/${variant.name}")
10 changes: 10 additions & 0 deletions build-logic/src/main/kotlin/PublishingLock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters

/**
* Lets only one publish task run at a time.
*
* The Minecraft version projects of a platform publish the same artifact in different versions,
* publishing them in parallel corrupts the shared maven-metadata.xml.
*/
abstract class PublishingLock : BuildService<BuildServiceParameters.None>
60 changes: 60 additions & 0 deletions build-logic/src/main/kotlin/faststats.fabric-compat.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import net.fabricmc.loom.api.LoomGradleExtensionAPI
import net.fabricmc.loom.task.RemapJarTask

plugins {
id("faststats.minecraft-version")
id("faststats.onboarding-sources")
id("faststats.shadow-conventions")
id("maven-publish")
}

// version projects are configured by their parent before they have a catalog of their own
val libs = rootProject.the<VersionCatalogsExtension>().named("libs")
val minecraft = minecraftVersion

pluginManager.apply(if (minecraft.obfuscated) "net.fabricmc.fabric-loom-remap" else "net.fabricmc.fabric-loom")
val loom = the<LoomGradleExtensionAPI>()

extra.set("publishArtifactId", "fabric")
extra.set("publishDocsUrl", "https://docs.faststats.dev/java/platform/fabric")
extra.set("publishVersionSuffix", "mc$minecraft")

tasks.processResources {
from(project(":fabric").file("src/main/resources"))
val version = provider { project.version.toString() }
val minecraftVersion = minecraft.name
inputs.property("version", version)
inputs.property("minecraft", minecraftVersion)
filesMatching("fabric.mod.json") {
expand("version" to version.get(), "minecraft" to minecraftVersion)
}
}

dependencies {
add("minecraft", libs.findLibrary("minecraft-${minecraft.catalogName}").get())
if (minecraft.obfuscated) add("mappings", loom.officialMojangMappings())

compileOnly(libs.findLibrary("fabric-api-${minecraft.fabricApiCatalogName}").get())
compileOnly(libs.findLibrary("fabric-loader").get())
compileOnly(libs.findLibrary("sponge-mixin").get())

implementation(project(":core"))
implementation(project(":config"))
implementation(project(":onboarding"))
implementation(project(":fabric", "runtimeElements"))
}

pluginManager.withPlugin("net.fabricmc.fabric-loom-remap") {
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
destinationDirectory.set(layout.buildDirectory.dir("devlibs"))
archiveClassifier.set("all")
}
val remapJar = tasks.named<RemapJarTask>("remapJar") {
inputFile.set(shadowJar.flatMap { it.archiveFile })
}
configurations.named("shadowRuntimeElements") {
outgoing.artifacts.clear()
outgoing.artifact(remapJar)
}
}
Loading