From 3523fd7eaf0a269de60beed1dc3e1b7479ddf833 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 15 Oct 2026 15:53:26 +0200 Subject: [PATCH 1/2] Add forge platform support --- CONTRIBUTING.md | 35 +++-- README.md | 10 +- build-logic/build.gradle.kts | 1 + .../src/main/kotlin/MinecraftVersions.kt | 3 +- .../kotlin/faststats.fabric-compat.gradle.kts | 1 - .../kotlin/faststats.forge-compat.gradle.kts | 67 ++++++++ ...faststats.mod-projects.settings.gradle.kts | 1 + .../faststats.neoforge-compat.gradle.kts | 1 - forge/build.gradle.kts | 26 ++++ .../dev/faststats/forge/ForgeContext.java | 143 ++++++++++++++++++ .../dev/faststats/forge/ForgeMetrics.java | 23 +++ .../faststats/forge/ForgeMetricsClient.java | 22 +++ .../faststats/forge/ForgeMetricsServer.java | 17 +++ .../forge/compat/CompatibilityLayer.java | 29 ++++ forge/src/main/java/module-info.java | 19 +++ forge/src/main/resources/META-INF/mods.toml | 20 +++ forge/src/main/resources/faststats.png | 1 + forge/src/main/resources/pack.mcmeta | 14 ++ .../dev/faststats/forge/ForgeEntryPoint.java | 10 ++ .../v1_20_6/CompatibilityLayerImpl.java | 77 ++++++++++ ....faststats.forge.compat.CompatibilityLayer | 1 + .../v1_21_6/CompatibilityLayerImpl.java | 76 ++++++++++ ....faststats.forge.compat.CompatibilityLayer | 1 + .../v1_21_9/CompatibilityLayerImpl.java | 76 ++++++++++ ....faststats.forge.compat.CompatibilityLayer | 1 + .../compat/v26_1/CompatibilityLayerImpl.java | 76 ++++++++++ ....faststats.forge.compat.CompatibilityLayer | 1 + gradle/libs.versions.toml | 21 +++ 28 files changed, 750 insertions(+), 23 deletions(-) create mode 100644 build-logic/src/main/kotlin/faststats.forge-compat.gradle.kts create mode 100644 forge/build.gradle.kts create mode 100644 forge/src/main/java/dev/faststats/forge/ForgeContext.java create mode 100644 forge/src/main/java/dev/faststats/forge/ForgeMetrics.java create mode 100644 forge/src/main/java/dev/faststats/forge/ForgeMetricsClient.java create mode 100644 forge/src/main/java/dev/faststats/forge/ForgeMetricsServer.java create mode 100644 forge/src/main/java/dev/faststats/forge/compat/CompatibilityLayer.java create mode 100644 forge/src/main/java/module-info.java create mode 100644 forge/src/main/resources/META-INF/mods.toml create mode 120000 forge/src/main/resources/faststats.png create mode 100644 forge/src/main/resources/pack.mcmeta create mode 100644 forge/src/mod/java/dev/faststats/forge/ForgeEntryPoint.java create mode 100644 forge/versions/1.20.6/src/main/java/dev/faststats/forge/compat/v1_20_6/CompatibilityLayerImpl.java create mode 100644 forge/versions/1.20.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer create mode 100644 forge/versions/1.21.6/src/main/java/dev/faststats/forge/compat/v1_21_6/CompatibilityLayerImpl.java create mode 100644 forge/versions/1.21.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer create mode 100644 forge/versions/1.21.9/src/main/java/dev/faststats/forge/compat/v1_21_9/CompatibilityLayerImpl.java create mode 100644 forge/versions/1.21.9/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer create mode 100644 forge/versions/26.1/src/main/java/dev/faststats/forge/compat/v26_1/CompatibilityLayerImpl.java create mode 100644 forge/versions/26.1/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba0feee8..1d231b27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,29 +8,31 @@ Run Gradle from the repository root: ./gradlew build ``` -This compiles, tests, and packages every module, including the Fabric and NeoForge artifacts for every supported +This compiles, tests, and packages every module, including the Fabric, Forge 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 :forge: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::+mc`. Each artifact only declares the +Fabric, Forge and NeoForge are built and published once per Minecraft version, as +`dev.faststats.metrics::+mc`. 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/` | Fabric code that depends on the Minecraft version | -| `neoforge/versions/` | NeoForge code that depends on the Minecraft version | -| `onboarding/versions/` | The consent screen and its mixins, compiled into both of them | +| Directory | Contents | +|----------------------------------|--------------------------------------------------------------| +| `fabric/`, `forge/`, `neoforge/` | Platform code that is the same for every Minecraft version | +| `onboarding/` | Onboarding code without any Minecraft dependency | +| `fabric/versions/` | Fabric code that depends on the Minecraft version | +| `forge/versions/` | Forge code that depends on the Minecraft version | +| `neoforge/versions/` | NeoForge code that depends on the Minecraft version | +| `onboarding/versions/` | The consent screen and its mixins, compiled into all of them | The directories in `/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 @@ -40,20 +42,20 @@ Each Minecraft version still gets its own Gradle project, `::versions: against exactly that version. Their directories in `/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 +The onboarding sources are compiled into the Fabric, Forge 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 +- **Fabric, Forge 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..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`. + `fabric-api-v1x21x10`, `forge-v1x21x10` and `neoforge-v1x21x10`. ## Adding a Minecraft version @@ -61,6 +63,7 @@ them through the `sharedSources` table in `build-logic/src/main/kotlin/faststats ```toml [versions] + forge-v26x4 = "26.4-67.0.0" neoforge-v26x4 = "26.4.0.1" [libraries] @@ -82,7 +85,7 @@ them through the `sharedSources` table in `build-logic/src/main/kotlin/faststats 3. Build the new version: ```sh - ./gradlew :fabric:versions:26.4:build :neoforge:versions:26.4:build + ./gradlew :fabric:versions:26.4:build :forge:versions:26.4:build :neoforge:versions:26.4:build ``` If it builds, the version is done. It is published with the next release. @@ -92,11 +95,11 @@ If it builds, the version is done. It is published with the next release. 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 +2. For Fabric, Forge 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 +A variant only replaces the code of its own platform. A new Fabric variant doesn't affect Forge, NeoForge or onboarding, and the other way around. diff --git a/README.md b/README.md index a8905f93..3f773319 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,17 @@ Use `build` for the reusable FastStats libraries: ./gradlew :velocity:build ``` -Library jars are written to each module's `build/libs` directory. Fabric and NeoForge compatibility artifacts are +Library jars are written to each module's `build/libs` directory. Fabric, Forge and NeoForge compatibility artifacts are published under stable Maven artifact IDs for every supported Minecraft version: ```text -dev.faststats.metrics:fabric:+mc26.1.2 -dev.faststats.metrics:neoforge:+mc26.1.2 +dev.faststats.metrics:fabric:+mc26.3 +dev.faststats.metrics:forge:+mc26.3 +dev.faststats.metrics:neoforge:+mc26.3 ``` -Each artifact only declares the Minecraft version it was built for in its `fabric.mod.json` or `neoforge.mods.toml`. +Each artifact only declares the Minecraft version it was built for in its `fabric.mod.json`, `mods.toml` or +`neoforge.mods.toml`. See [CONTRIBUTING.md](CONTRIBUTING.md) for how the version-specific code is organized and how to add a Minecraft version. diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 210bf6b5..cb1b85ad 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -14,6 +14,7 @@ fun plugin(plugin: Provider) = plugin.map { dependencies { implementation(plugin(libs.plugins.fabric.loom)) + implementation(plugin(libs.plugins.forgegradle)) implementation(plugin(libs.plugins.moddev)) implementation(plugin(libs.plugins.shadow)) } diff --git a/build-logic/src/main/kotlin/MinecraftVersions.kt b/build-logic/src/main/kotlin/MinecraftVersions.kt index 67060742..62f75723 100644 --- a/build-logic/src/main/kotlin/MinecraftVersions.kt +++ b/build-logic/src/main/kotlin/MinecraftVersions.kt @@ -70,11 +70,12 @@ val minecraftVersions = listOf( private val platformVersions = mapOf( "fabric" to minecraftVersions, "neoforge" to minecraftVersions.dropWhile { it.name != "1.20.6" }, + "forge" to minecraftVersions.dropWhile { it.name != "1.20.6" }.filter { it.name != "1.21.2" }, "onboarding" to minecraftVersions, ) // the platforms with a project per Minecraft version, the onboarding sources are compiled into them -val minecraftPlatforms = setOf("fabric", "neoforge") +val minecraftPlatforms = setOf("fabric", "forge", "neoforge") fun minecraftVersions(platform: String): List = platformVersions.getValue(platform) diff --git a/build-logic/src/main/kotlin/faststats.fabric-compat.gradle.kts b/build-logic/src/main/kotlin/faststats.fabric-compat.gradle.kts index fafc9ffe..f28b8855 100644 --- a/build-logic/src/main/kotlin/faststats.fabric-compat.gradle.kts +++ b/build-logic/src/main/kotlin/faststats.fabric-compat.gradle.kts @@ -9,7 +9,6 @@ plugins { id("maven-publish") } -// version projects are configured by their parent before they have a catalog of their own val libs = rootProject.the().named("libs") val minecraft = minecraftVersion diff --git a/build-logic/src/main/kotlin/faststats.forge-compat.gradle.kts b/build-logic/src/main/kotlin/faststats.forge-compat.gradle.kts new file mode 100644 index 00000000..99b3bfcd --- /dev/null +++ b/build-logic/src/main/kotlin/faststats.forge-compat.gradle.kts @@ -0,0 +1,67 @@ +import net.minecraftforge.gradle.ForgeGradleExtension +import net.minecraftforge.gradle.MinecraftExtensionForProject + +plugins { + id("net.minecraftforge.gradle") + id("faststats.minecraft-version") + id("faststats.onboarding-sources") + id("faststats.shadow-conventions") + id("maven-publish") +} + +val libs = rootProject.the().named("libs") +val minecraft = minecraftVersion + +extra.set("publishArtifactId", "forge") +extra.set("publishDocsUrl", "https://docs.faststats.dev/java/platform/forge") +extra.set("publishVersionSuffix", "mc$minecraft") + +val forgeGradle = the() +val minecraftExtension = the() + +if (minecraft.obfuscated) minecraftExtension.mappings("official", minecraft.name) + +repositories { + minecraftExtension.mavenizer(this) + maven(forgeGradle.forgeMaven) + maven(forgeGradle.minecraftLibsMaven) +} + +sourceSets.main { + java.srcDir(project(":forge").file("src/mod/java")) +} + +tasks.processResources { + from(project(":forge").file("src/main/resources")) + val version = provider { project.version.toString() } + val range = "[${minecraft.name}]" + inputs.property("version", version) + inputs.property("minecraft", range) + filesMatching("META-INF/mods.toml") { + expand("version" to version.get(), "minecraft" to range) + } + // Forge ships Mixin 0.8.5 (1.20.6) or 0.8.7, which don't know compatibility levels above JAVA_18 or JAVA_21 + val maxMixinLevel = if (minecraft.name == "1.20.6") 18 else 21 + inputs.property("maxMixinLevel", maxMixinLevel) + filesMatching("faststats-onboarding.mixins.json") { + filter { line -> + line.replace(Regex("\"JAVA_(\\d+)\"")) { level -> + if (level.groupValues[1].toInt() > maxMixinLevel) "\"JAVA_$maxMixinLevel\"" else level.value + } + } + } +} + +tasks.withType().configureEach { + manifest.attributes("MixinConfigs" to "faststats-onboarding.mixins.json") +} + +val forge = libs.findVersion("forge-${minecraft.catalogName}").get().requiredVersion + +dependencies { + compileOnly(minecraftExtension.dependency("net.minecraftforge:forge:$forge")) + implementation(project(":core")) + implementation(project(":config")) + implementation(project(":onboarding")) + implementation(project(":forge")) +} diff --git a/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts b/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts index 8174f93c..b16cff4a 100644 --- a/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts +++ b/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts @@ -1,5 +1,6 @@ include( "fabric", + "forge", "neoforge", "onboarding" ) diff --git a/build-logic/src/main/kotlin/faststats.neoforge-compat.gradle.kts b/build-logic/src/main/kotlin/faststats.neoforge-compat.gradle.kts index 57611b28..5c68ab67 100644 --- a/build-logic/src/main/kotlin/faststats.neoforge-compat.gradle.kts +++ b/build-logic/src/main/kotlin/faststats.neoforge-compat.gradle.kts @@ -6,7 +6,6 @@ plugins { id("maven-publish") } -// version projects are configured by their parent before they have a catalog of their own val libs = rootProject.the().named("libs") val minecraft = minecraftVersion diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts new file mode 100644 index 00000000..98f31f1c --- /dev/null +++ b/forge/build.gradle.kts @@ -0,0 +1,26 @@ +extra.set("moduleName", "dev.faststats.forge") +extra.set("publishArtifactId", "forge") +extra.set("publishDocsUrl", "https://docs.faststats.dev/java/platform/forge") + +plugins { + id("faststats.shadow-conventions") + id("maven-publish") +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(25) + +tasks.compileJava { + options.release.set(21) +} + +dependencies { + implementation(project(":core")) + implementation(project(":config")) + compileOnlyApi(project(":onboarding")) + compileOnly(libs.gson) + compileOnly(libs.slf4j.api) +} + +subprojects { + if (parent?.path == ":forge:versions") pluginManager.apply("faststats.forge-compat") +} diff --git a/forge/src/main/java/dev/faststats/forge/ForgeContext.java b/forge/src/main/java/dev/faststats/forge/ForgeContext.java new file mode 100644 index 00000000..e0324efe --- /dev/null +++ b/forge/src/main/java/dev/faststats/forge/ForgeContext.java @@ -0,0 +1,143 @@ +package dev.faststats.forge; + +import dev.faststats.screen.registry.FastStatsRegistry; +import dev.faststats.Metrics; +import dev.faststats.SimpleContext; +import dev.faststats.SimpleMetrics; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; +import dev.faststats.forge.compat.CompatibilityLayer; +import dev.faststats.internal.LoggerFactory; +import dev.faststats.internal.PlatformLoggerFactory; +import org.jetbrains.annotations.Contract; + +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Forge FastStats context. + * + * @since 0.31.0 + */ +public final class ForgeContext extends SimpleContext { + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(runnable -> { + final var thread = new Thread(runnable, "faststats-submitter"); + thread.setDaemon(true); + return thread; + }); + private final Set> tasks = new CopyOnWriteArraySet<>(); + private final CompatibilityLayer compatibilityLayer; + private final String modId; + private final String modVersion; + private final boolean consentRequired; + + private ForgeContext(final Factory factory, final LoggerFactory loggerFactory, final String modId, @Token final String token) { + this(factory, loggerFactory, modId, token, ServiceLoader.load(CompatibilityLayer.class) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No Forge compatibility layer found"))); + } + + private ForgeContext(final Factory factory, final LoggerFactory loggerFactory, final String modId, @Token final String token, final CompatibilityLayer compatibilityLayer) { + super(factory, loggerFactory, SimpleConfig.read( + compatibilityLayer.configDirectory().resolve("faststats").resolve("config.properties"), loggerFactory, + compatibilityLayer.getEnvironment() == CompatibilityLayer.Environment.CLIENT), "forge", token); + this.modVersion = compatibilityLayer.modVersion(modId).orElseThrow(() -> { + return new IllegalArgumentException("Mod not found: " + modId); + }); + this.modId = modId; + this.compatibilityLayer = compatibilityLayer; + this.consentRequired = factory.consentRequired; + initializeManagedServices(factory); + FastStatsRegistry.instance().register(this, factory.consentRequired); + switch (compatibilityLayer.getEnvironment()) { + case CLIENT -> ready(); + case SERVER -> compatibilityLayer.onServerLifecycle(this::ready, this::shutdown); + } + } + + @Override + @Contract(value = " -> new", pure = true) + protected Metrics.Factory metricsFactory() { + return new SimpleMetrics.Factory(this) { + @Override + public Metrics create() throws IllegalStateException { + return switch (compatibilityLayer.getEnvironment()) { + case CLIENT -> new ForgeMetricsClient(this, modVersion, compatibilityLayer); + case SERVER -> new ForgeMetricsServer(this, modVersion, compatibilityLayer); + }; + } + }; + } + + @Override + protected boolean preSubmissionStart() { + final var config = (SimpleConfig) getConfig(); + if (!config.preSubmissionStart(this)) return false; + if (!consentRequired || config.consentScreenShown()) return true; + return compatibilityLayer.getEnvironment() != CompatibilityLayer.Environment.CLIENT; + } + + @Override + public String getProjectName() { + return modId; + } + + @Override + protected void scheduleAtFixedRate(final Runnable task, final long initialDelay, final long period, final TimeUnit unit) { + tasks.add(executor.scheduleAtFixedRate(task, initialDelay, period, unit)); + } + + @Override + public void shutdown() { + FastStatsRegistry.instance().unregister(this); + super.shutdown(); + tasks.forEach(task -> task.cancel(true)); + tasks.clear(); + executor.shutdown(); + } + + public static final class Factory extends SimpleContext.Factory { + private final String modId; + private final @Token String token; + private boolean consentRequired = false; + + public Factory(final String modId, @Token final String token) { + this.modId = modId; + this.token = token; + } + + /** + * Requires the user to explicitly give consent on the client. + *

+ * When at least one mod requires consent, the consent screen is opened + * on the client until it was shown once, nothing is submitted before that. + * By default, no consent screen is shown. + * + * @return this factory + * @since 0.31.0 + */ + @Contract(value = " -> this", mutates = "this") + public Factory requireConsent() { + this.consentRequired = true; + return this; + } + + @Override + public ForgeContext create() { + final var logger = org.slf4j.LoggerFactory.getLogger(modId); + final var loggerFactory = new PlatformLoggerFactory((level, throwable, message) -> { + switch (level) { + case INFO -> logger.info(message, throwable); + case ERROR -> logger.error(message, throwable); + case WARN -> logger.warn(message, throwable); + } + }); + return new ForgeContext(this, loggerFactory, modId, token); + } + } +} diff --git a/forge/src/main/java/dev/faststats/forge/ForgeMetrics.java b/forge/src/main/java/dev/faststats/forge/ForgeMetrics.java new file mode 100644 index 00000000..941160a9 --- /dev/null +++ b/forge/src/main/java/dev/faststats/forge/ForgeMetrics.java @@ -0,0 +1,23 @@ +package dev.faststats.forge; + +import com.google.gson.JsonObject; +import dev.faststats.SimpleMetrics; +import dev.faststats.forge.compat.CompatibilityLayer; + +abstract class ForgeMetrics extends SimpleMetrics { + protected final String modVersion; + protected final CompatibilityLayer compatibilityLayer; + + protected ForgeMetrics(final Factory factory, final String modVersion, final CompatibilityLayer compatibilityLayer) throws IllegalStateException { + super(factory); + this.modVersion = modVersion; + this.compatibilityLayer = compatibilityLayer; + } + + protected void appendForgeData(final JsonObject metrics, final String serverType) { + compatibilityLayer.modVersion("minecraft").ifPresent(s -> metrics.addProperty("game_version", s)); + compatibilityLayer.modVersion("forge").ifPresent(s -> metrics.addProperty("platform_version", s)); + metrics.addProperty("plugin_version", modVersion); + metrics.addProperty("server_type", serverType); + } +} diff --git a/forge/src/main/java/dev/faststats/forge/ForgeMetricsClient.java b/forge/src/main/java/dev/faststats/forge/ForgeMetricsClient.java new file mode 100644 index 00000000..c5fb1c7d --- /dev/null +++ b/forge/src/main/java/dev/faststats/forge/ForgeMetricsClient.java @@ -0,0 +1,22 @@ +package dev.faststats.forge; + +import com.google.gson.JsonObject; +import dev.faststats.forge.compat.CompatibilityLayer; + +final class ForgeMetricsClient extends ForgeMetrics { + ForgeMetricsClient(final Factory factory, final String modVersion, final CompatibilityLayer compatibilityLayer) throws IllegalStateException { + super(factory, modVersion, compatibilityLayer); + } + + @Override + public boolean isClientApplication() { + return true; + } + + @Override + protected void appendDefaultData(final JsonObject metrics) { + metrics.addProperty("online_mode", compatibilityLayer.clientOnlineMode()); + metrics.addProperty("player_count", compatibilityLayer.clientPlayerCount()); + appendForgeData(metrics, "Forge Client"); + } +} diff --git a/forge/src/main/java/dev/faststats/forge/ForgeMetricsServer.java b/forge/src/main/java/dev/faststats/forge/ForgeMetricsServer.java new file mode 100644 index 00000000..f6c0488f --- /dev/null +++ b/forge/src/main/java/dev/faststats/forge/ForgeMetricsServer.java @@ -0,0 +1,17 @@ +package dev.faststats.forge; + +import com.google.gson.JsonObject; +import dev.faststats.forge.compat.CompatibilityLayer; + +final class ForgeMetricsServer extends ForgeMetrics { + ForgeMetricsServer(final Factory factory, final String modVersion, final CompatibilityLayer compatibilityLayer) throws IllegalStateException { + super(factory, modVersion, compatibilityLayer); + } + + @Override + protected void appendDefaultData(final JsonObject metrics) { + metrics.addProperty("online_mode", compatibilityLayer.serverOnlineMode()); + metrics.addProperty("player_count", compatibilityLayer.serverPlayerCount()); + appendForgeData(metrics, "Forge"); + } +} diff --git a/forge/src/main/java/dev/faststats/forge/compat/CompatibilityLayer.java b/forge/src/main/java/dev/faststats/forge/compat/CompatibilityLayer.java new file mode 100644 index 00000000..ed79279b --- /dev/null +++ b/forge/src/main/java/dev/faststats/forge/compat/CompatibilityLayer.java @@ -0,0 +1,29 @@ +package dev.faststats.forge.compat; + +import java.nio.file.Path; +import java.util.Optional; + +public interface CompatibilityLayer { + /** + * Runs the given tasks once the dedicated server started and when it is stopping. + */ + void onServerLifecycle(Runnable started, Runnable stopping); + + boolean clientOnlineMode(); + + int clientPlayerCount(); + + boolean serverOnlineMode(); + + int serverPlayerCount(); + + Optional modVersion(String modId); + + Path configDirectory(); + + Environment getEnvironment(); + + enum Environment { + CLIENT, SERVER + } +} diff --git a/forge/src/main/java/module-info.java b/forge/src/main/java/module-info.java new file mode 100644 index 00000000..0505798f --- /dev/null +++ b/forge/src/main/java/module-info.java @@ -0,0 +1,19 @@ +import org.jspecify.annotations.NullMarked; + +@NullMarked +module dev.faststats.forge { + exports dev.faststats.forge.compat; + exports dev.faststats.forge; + + requires com.google.gson; + requires dev.faststats.config; + requires dev.faststats.onboarding; + requires dev.faststats; + requires java.net.http; + requires org.slf4j; + + requires static org.jetbrains.annotations; + requires static org.jspecify; + + uses dev.faststats.forge.compat.CompatibilityLayer; +} diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 00000000..39aef295 --- /dev/null +++ b/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,20 @@ +modLoader = "javafml" +loaderVersion = "[0,)" +license = "MIT" +issueTrackerURL = "https://github.com/faststats-dev/faststats-java/issues" + +[[mods]] +modId = "faststats" +version = "${version}" +displayName = "FastStats" +description = "Analytics for developers" +logoFile = "faststats.png" +authors = "David | NonSwag" +displayURL = "https://faststats.dev/mods" + +[[dependencies.faststats]] +modId = "minecraft" +mandatory = true +versionRange = "${minecraft}" +ordering = "NONE" +side = "BOTH" diff --git a/forge/src/main/resources/faststats.png b/forge/src/main/resources/faststats.png new file mode 120000 index 00000000..02b53612 --- /dev/null +++ b/forge/src/main/resources/faststats.png @@ -0,0 +1 @@ +../../../../assets/faststats.png \ No newline at end of file diff --git a/forge/src/main/resources/pack.mcmeta b/forge/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..259f8913 --- /dev/null +++ b/forge/src/main/resources/pack.mcmeta @@ -0,0 +1,14 @@ +{ + "pack": { + "description": { + "text": "FastStats Mod resources" + }, + "pack_format": 32, + "supported_formats": [ + 32, + 121 + ], + "min_format": 32, + "max_format": 121 + } +} diff --git a/forge/src/mod/java/dev/faststats/forge/ForgeEntryPoint.java b/forge/src/mod/java/dev/faststats/forge/ForgeEntryPoint.java new file mode 100644 index 00000000..8622b53f --- /dev/null +++ b/forge/src/mod/java/dev/faststats/forge/ForgeEntryPoint.java @@ -0,0 +1,10 @@ +package dev.faststats.forge; + +import net.minecraftforge.fml.common.Mod; + +/** + * Forge requires every mod declared in the {@code mods.toml} to have an entry point annotated with {@link Mod}. + */ +@Mod("faststats") +public final class ForgeEntryPoint { +} diff --git a/forge/versions/1.20.6/src/main/java/dev/faststats/forge/compat/v1_20_6/CompatibilityLayerImpl.java b/forge/versions/1.20.6/src/main/java/dev/faststats/forge/compat/v1_20_6/CompatibilityLayerImpl.java new file mode 100644 index 00000000..9e233d11 --- /dev/null +++ b/forge/versions/1.20.6/src/main/java/dev/faststats/forge/compat/v1_20_6/CompatibilityLayerImpl.java @@ -0,0 +1,77 @@ +package dev.faststats.forge.compat.v1_20_6; + +import dev.faststats.forge.compat.CompatibilityLayer; +import net.minecraft.client.Minecraft; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Optional; + +public final class CompatibilityLayerImpl implements CompatibilityLayer { + private @Nullable MinecraftServer server; + + @Override + public void onServerLifecycle(final Runnable started, final Runnable stopping) { + MinecraftForge.EVENT_BUS.addListener((final ServerStartedEvent event) -> { + this.server = event.getServer(); + started.run(); + }); + MinecraftForge.EVENT_BUS.addListener((final ServerStoppingEvent event) -> stopping.run()); + } + + @Override + public boolean clientOnlineMode() { + final var client = Minecraft.getInstance(); + return client.getUser().getXuid().isPresent(); // todo: offline dev mode? + } + + @Override + public int clientPlayerCount() { + final var client = Minecraft.getInstance(); + final var connection = client.getConnection(); + if (connection != null) return connection.getOnlinePlayers().size(); + + final var server = client.getSingleplayerServer(); + if (server != null) return server.getPlayerCount(); + + return client.player == null ? 0 : 1; + } + + @Override + public boolean serverOnlineMode() { + assert server != null : "Server not initialized"; + return server.usesAuthentication(); + } + + @Override + public int serverPlayerCount() { + assert server != null : "Server not initialized"; + return server.getPlayerCount(); + } + + @Override + public Optional modVersion(final String modId) { + return ModList.get().getModContainerById(modId) + .map(container -> container.getModInfo().getVersion().toString()); + } + + @Override + public Path configDirectory() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public Environment getEnvironment() { + return switch (FMLEnvironment.dist) { + case CLIENT -> Environment.CLIENT; + case DEDICATED_SERVER -> Environment.SERVER; + }; + } +} diff --git a/forge/versions/1.20.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer b/forge/versions/1.20.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer new file mode 100644 index 00000000..e788695f --- /dev/null +++ b/forge/versions/1.20.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer @@ -0,0 +1 @@ +dev.faststats.forge.compat.v1_20_6.CompatibilityLayerImpl diff --git a/forge/versions/1.21.6/src/main/java/dev/faststats/forge/compat/v1_21_6/CompatibilityLayerImpl.java b/forge/versions/1.21.6/src/main/java/dev/faststats/forge/compat/v1_21_6/CompatibilityLayerImpl.java new file mode 100644 index 00000000..a9d67aca --- /dev/null +++ b/forge/versions/1.21.6/src/main/java/dev/faststats/forge/compat/v1_21_6/CompatibilityLayerImpl.java @@ -0,0 +1,76 @@ +package dev.faststats.forge.compat.v1_21_6; + +import dev.faststats.forge.compat.CompatibilityLayer; +import net.minecraft.client.Minecraft; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Optional; + +public final class CompatibilityLayerImpl implements CompatibilityLayer { + private @Nullable MinecraftServer server; + + @Override + public void onServerLifecycle(final Runnable started, final Runnable stopping) { + ServerStartedEvent.BUS.addListener(event -> { + this.server = event.getServer(); + started.run(); + }); + ServerStoppingEvent.BUS.addListener(event -> stopping.run()); + } + + @Override + public boolean clientOnlineMode() { + final var client = Minecraft.getInstance(); + return client.getUser().getXuid().isPresent(); // todo: offline dev mode? + } + + @Override + public int clientPlayerCount() { + final var client = Minecraft.getInstance(); + final var connection = client.getConnection(); + if (connection != null) return connection.getOnlinePlayers().size(); + + final var server = client.getSingleplayerServer(); + if (server != null) return server.getPlayerCount(); + + return client.player == null ? 0 : 1; + } + + @Override + public boolean serverOnlineMode() { + assert server != null : "Server not initialized"; + return server.usesAuthentication(); + } + + @Override + public int serverPlayerCount() { + assert server != null : "Server not initialized"; + return server.getPlayerCount(); + } + + @Override + public Optional modVersion(final String modId) { + return ModList.get().getModContainerById(modId) + .map(container -> container.getModInfo().getVersion().toString()); + } + + @Override + public Path configDirectory() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public Environment getEnvironment() { + return switch (FMLEnvironment.dist) { + case CLIENT -> Environment.CLIENT; + case DEDICATED_SERVER -> Environment.SERVER; + }; + } +} diff --git a/forge/versions/1.21.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer b/forge/versions/1.21.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer new file mode 100644 index 00000000..a036b69b --- /dev/null +++ b/forge/versions/1.21.6/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer @@ -0,0 +1 @@ +dev.faststats.forge.compat.v1_21_6.CompatibilityLayerImpl diff --git a/forge/versions/1.21.9/src/main/java/dev/faststats/forge/compat/v1_21_9/CompatibilityLayerImpl.java b/forge/versions/1.21.9/src/main/java/dev/faststats/forge/compat/v1_21_9/CompatibilityLayerImpl.java new file mode 100644 index 00000000..5da45622 --- /dev/null +++ b/forge/versions/1.21.9/src/main/java/dev/faststats/forge/compat/v1_21_9/CompatibilityLayerImpl.java @@ -0,0 +1,76 @@ +package dev.faststats.forge.compat.v1_21_9; + +import dev.faststats.forge.compat.CompatibilityLayer; +import net.minecraft.client.Minecraft; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Optional; + +public final class CompatibilityLayerImpl implements CompatibilityLayer { + private @Nullable MinecraftServer server; + + @Override + public void onServerLifecycle(final Runnable started, final Runnable stopping) { + ServerStartedEvent.BUS.addListener(event -> { + this.server = event.getServer(); + started.run(); + }); + ServerStoppingEvent.BUS.addListener(event -> stopping.run()); + } + + @Override + public boolean clientOnlineMode() { + final var client = Minecraft.getInstance(); + return client.getUser().getXuid().isPresent() && !client.isOfflineDeveloperMode(); + } + + @Override + public int clientPlayerCount() { + final var client = Minecraft.getInstance(); + final var connection = client.getConnection(); + if (connection != null) return connection.getOnlinePlayers().size(); + + final var server = client.getSingleplayerServer(); + if (server != null) return server.getPlayerCount(); + + return client.player == null ? 0 : 1; + } + + @Override + public boolean serverOnlineMode() { + assert server != null : "Server not initialized"; + return server.usesAuthentication(); + } + + @Override + public int serverPlayerCount() { + assert server != null : "Server not initialized"; + return server.getPlayerCount(); + } + + @Override + public Optional modVersion(final String modId) { + return ModList.get().getModContainerById(modId) + .map(container -> container.getModInfo().getVersion().toString()); + } + + @Override + public Path configDirectory() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public Environment getEnvironment() { + return switch (FMLEnvironment.dist) { + case CLIENT -> Environment.CLIENT; + case DEDICATED_SERVER -> Environment.SERVER; + }; + } +} diff --git a/forge/versions/1.21.9/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer b/forge/versions/1.21.9/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer new file mode 100644 index 00000000..b3d711a7 --- /dev/null +++ b/forge/versions/1.21.9/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer @@ -0,0 +1 @@ +dev.faststats.forge.compat.v1_21_9.CompatibilityLayerImpl diff --git a/forge/versions/26.1/src/main/java/dev/faststats/forge/compat/v26_1/CompatibilityLayerImpl.java b/forge/versions/26.1/src/main/java/dev/faststats/forge/compat/v26_1/CompatibilityLayerImpl.java new file mode 100644 index 00000000..851fc044 --- /dev/null +++ b/forge/versions/26.1/src/main/java/dev/faststats/forge/compat/v26_1/CompatibilityLayerImpl.java @@ -0,0 +1,76 @@ +package dev.faststats.forge.compat.v26_1; + +import dev.faststats.forge.compat.CompatibilityLayer; +import net.minecraft.client.Minecraft; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.loading.FMLPaths; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Optional; + +public final class CompatibilityLayerImpl implements CompatibilityLayer { + private @Nullable MinecraftServer server; + + @Override + public void onServerLifecycle(final Runnable started, final Runnable stopping) { + ServerStartedEvent.BUS.addListener(event -> { + this.server = event.getServer(); + started.run(); + }); + ServerStoppingEvent.BUS.addListener(event -> stopping.run()); + } + + @Override + public boolean clientOnlineMode() { + final var client = Minecraft.getInstance(); + return client.getUser().getXuid().isPresent() && !client.isOfflineDeveloperMode(); + } + + @Override + public int clientPlayerCount() { + final var client = Minecraft.getInstance(); + final var connection = client.getConnection(); + if (connection != null) return connection.getOnlinePlayers().size(); + + final var server = client.getSingleplayerServer(); + if (server != null) return server.getPlayerCount(); + + return client.player == null ? 0 : 1; + } + + @Override + public boolean serverOnlineMode() { + assert server != null : "Server not initialized"; + return server.usesAuthentication(); + } + + @Override + public int serverPlayerCount() { + assert server != null : "Server not initialized"; + return server.getPlayerCount(); + } + + @Override + public Optional modVersion(final String modId) { + return ModList.getModContainerById(modId) + .map(container -> container.getModInfo().getVersion().toString()); + } + + @Override + public Path configDirectory() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public Environment getEnvironment() { + return switch (FMLEnvironment.dist) { + case CLIENT -> Environment.CLIENT; + case DEDICATED_SERVER -> Environment.SERVER; + }; + } +} diff --git a/forge/versions/26.1/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer b/forge/versions/26.1/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer new file mode 100644 index 00000000..64f82628 --- /dev/null +++ b/forge/versions/26.1/src/main/resources/META-INF/services/dev.faststats.forge.compat.CompatibilityLayer @@ -0,0 +1 @@ +dev.faststats.forge.compat.v26_1.CompatibilityLayerImpl diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 264479ff..f53811a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,25 @@ neoforge-v26x1x2 = "26.1.2.116" neoforge-v26x2 = "26.2.0.90" neoforge-v26x3 = "26.3.0.76" +# Forge versions are passed to minecraft.dependency(...) of ForgeGradle +forge-v1x20x6 = "1.20.6-50.2.14" +forge-v1x21 = "1.21-51.0.33" +forge-v1x21x1 = "1.21.1-52.1.20" +forge-v1x21x3 = "1.21.3-53.1.16" +forge-v1x21x4 = "1.21.4-54.1.22" +forge-v1x21x5 = "1.21.5-55.1.18" +forge-v1x21x6 = "1.21.6-56.0.9" +forge-v1x21x7 = "1.21.7-57.0.3" +forge-v1x21x8 = "1.21.8-58.1.26" +forge-v1x21x9 = "1.21.9-59.0.5" +forge-v1x21x10 = "1.21.10-60.1.18" +forge-v1x21x11 = "1.21.11-61.2.4" +forge-v26x1 = "26.1-62.0.9" +forge-v26x1x1 = "26.1.1-63.0.2" +forge-v26x1x2 = "26.1.2-64.1.5" +forge-v26x2 = "26.2-65.1.5" +forge-v26x3 = "26.3-66.0.12" + [libraries] gson = "com.google.code.gson:gson:2.14.0" jetbrains-annotations = "org.jetbrains:annotations:26.1.0" @@ -39,6 +58,7 @@ fabric-loader = "net.fabricmc:fabric-loader:0.19.5" modlauncher = "cpw.mods:modlauncher:10.1.12" log4j-api = "org.apache.logging.log4j:log4j-api:2.24.3" neoforged-bus = "net.neoforged:bus:8.0.5" +slf4j-api = "org.slf4j:slf4j-api:2.0.17" sponge-mixin = "net.fabricmc:sponge-mixin:0.17.5+mixin.0.8.7" # minecraft versions compiled against by the version-ranged modules @@ -121,6 +141,7 @@ fabric-api-v26x2 = "net.fabricmc.fabric-api:fabric-api:0.153.0+26.2" fabric-api-v26x3 = "net.fabricmc.fabric-api:fabric-api:0.153.1+26.3" [plugins] +forgegradle = { id = "net.minecraftforge.gradle", version = "7.0.40" } fabric-loom = { id = "net.fabricmc.fabric-loom", version = "1.15-SNAPSHOT" } moddev = { id = "net.neoforged.moddev", version = "2.0.148" } shadow = { id = "com.gradleup.shadow", version = "9.6.1" } From d74d1b837a02e58ad6a9dceeafabb662cb95f520 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 15 Oct 2026 17:30:06 +0200 Subject: [PATCH 2/2] Parallelize build and publishing tasks and workflows --- .github/workflows/build-mods.yml | 28 ++++++++++++++++++ .github/workflows/build.yml | 2 +- .github/workflows/maven-publish-mods.yml | 29 +++++++++++++++++++ .github/workflows/maven-publish.yml | 2 +- CONTRIBUTING.md | 8 +++++ README.md | 6 ++++ ...faststats.mod-projects.settings.gradle.kts | 17 ++++++----- 7 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build-mods.yml create mode 100644 .github/workflows/maven-publish-mods.yml diff --git a/.github/workflows/build-mods.yml b/.github/workflows/build-mods.yml new file mode 100644 index 00000000..4c9d0905 --- /dev/null +++ b/.github/workflows/build-mods.yml @@ -0,0 +1,28 @@ +name: Build Mods +on: + push: + branches: + - main + pull_request: +jobs: + build: + name: Build ${{ matrix.platform }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: [ fabric, forge, neoforge ] + 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: Build with Gradle + run: ./gradlew build -PmodPlatforms=${{ matrix.platform }} -p ${{ matrix.platform }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 008c0416..7e4b68c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,4 +20,4 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build -PmodPlatforms=none diff --git a/.github/workflows/maven-publish-mods.yml b/.github/workflows/maven-publish-mods.yml new file mode 100644 index 00000000..fc328f8e --- /dev/null +++ b/.github/workflows/maven-publish-mods.yml @@ -0,0 +1,29 @@ +name: Maven Publish Mods +on: + release: + types: [ prereleased, released ] +jobs: + build: + name: Publish ${{ matrix.platform }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: [ fabric, forge, neoforge ] + 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: Publish with Gradle to Repository + env: + REPOSITORY_USER: ${{ secrets.REPOSITORY_USER }} + REPOSITORY_TOKEN: ${{ secrets.REPOSITORY_TOKEN }} + run: ./gradlew publish -PmodPlatforms=${{ matrix.platform }} -p ${{ matrix.platform }} diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 88dfc317..35a1c95c 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -21,4 +21,4 @@ jobs: env: REPOSITORY_USER: ${{ secrets.REPOSITORY_USER }} REPOSITORY_TOKEN: ${{ secrets.REPOSITORY_TOKEN }} - run: ./gradlew publish + run: ./gradlew publish -PmodPlatforms=none diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d231b27..d33545e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,14 @@ Minecraft version. To work on a single version, build only its project: ./gradlew :neoforge:versions:1.21.10:build ``` +The `modPlatforms` property limits which mod platforms are part of the build at all, which skips configuring them. +CI uses it to build the plugin modules and each mod platform in separate jobs: + +```sh +./gradlew build -PmodPlatforms=none # everything except Fabric, Forge and NeoForge +./gradlew build -PmodPlatforms=fabric -p fabric # only Fabric and its Minecraft versions +``` + ## Minecraft versions Fabric, Forge and NeoForge are built and published once per Minecraft version, as diff --git a/README.md b/README.md index 3f773319..3ca4d939 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,9 @@ To compile and test all modules with the standard lifecycle, run: ./gradlew build ``` +To build everything except the Fabric, Forge and NeoForge mod platforms, which is much faster, run: + +```sh +./gradlew build -PmodPlatforms=none +``` + diff --git a/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts b/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts index b16cff4a..d32d5e60 100644 --- a/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts +++ b/build-logic/src/main/kotlin/faststats.mod-projects.settings.gradle.kts @@ -1,9 +1,12 @@ -include( - "fabric", - "forge", - "neoforge", - "onboarding" -) +// -PmodPlatforms=fabric,forge limits which mod platforms are included, -PmodPlatforms=none excludes all of them +val modPlatforms = providers.gradleProperty("modPlatforms").orNull?.let { property -> + property.split(',').map(String::trim).filter { it.isNotEmpty() && it != "none" }.toSet().onEach { + require(it in minecraftPlatforms) { "Unknown mod platform \"$it\", expected one of $minecraftPlatforms" } + } +} ?: minecraftPlatforms + +include("onboarding") +modPlatforms.forEach { include(it) } (minecraftPlatforms + "onboarding").forEach { platform -> file("$platform/versions").listFiles { file -> file.resolve("src").isDirectory }?.forEach { variant -> @@ -14,7 +17,7 @@ include( } // one project per platform and Minecraft version, see MinecraftVersions.kt -minecraftPlatforms.forEach { platform -> +modPlatforms.forEach { platform -> minecraftVersions(platform).forEach { version -> val path = ":$platform:versions:${version.name}" include(path)