diff --git a/build.gradle.kts b/build.gradle.kts index 60f0dfce..c08913c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,8 @@ val javaVersionsOverride = mapOf( ":hytale:example-plugin" to 25, ":minestom" to 25, ":minestom:example-server" to 25, + ":neoforge" to 25, + ":neoforge:example-mod" to 25, ":velocity" to 21, ":velocity:example-plugin" to 21 ) @@ -25,7 +27,7 @@ subprojects { val example = project.name.startsWith("example") if (example) { - if (project.path != ":fabric:example-mod") { + if (project.path != ":fabric:example-mod" && project.path != ":neoforge:example-mod") { apply { plugin("com.gradleup.shadow") } } } else { diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricContext.java b/fabric/src/main/java/dev/faststats/fabric/FabricContext.java index a26672d0..369f3b53 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricContext.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricContext.java @@ -41,7 +41,7 @@ private FabricContext(final Factory factory, final String modId, @Token final St switch (FabricLoader.getInstance().getEnvironmentType()) { case CLIENT -> { ready(); - ClientLifecycleEvents.CLIENT_STARTED.register(client -> shutdown()); + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> shutdown()); } case SERVER -> { ServerLifecycleEvents.SERVER_STARTED.register(server -> ready()); diff --git a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java index 037bcc56..0aaef3b3 100644 --- a/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java +++ b/fabric/src/main/java/dev/faststats/fabric/FabricMetrics.java @@ -15,13 +15,22 @@ protected FabricMetrics(final Factory factory, final ModContainer mod) throws Il protected void appendFabricData(final JsonObject metrics, final String serverType) { metrics.addProperty("minecraft_version", minecraftVersion()); + metrics.addProperty("platform_version", platformVersion()); metrics.addProperty("plugin_version", mod.getMetadata().getVersion().getFriendlyString()); metrics.addProperty("server_type", serverType); } protected static String minecraftVersion() { + return version("minecraft"); + } + + protected static String platformVersion() { + return version("fabricloader"); + } + + private static String version(final String modId) { return FabricLoader.getInstance() - .getModContainer("minecraft") + .getModContainer(modId) .map(container -> container.getMetadata().getVersion().getFriendlyString()) .orElse("unknown"); } diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts new file mode 100644 index 00000000..766ec7c2 --- /dev/null +++ b/neoforge/build.gradle.kts @@ -0,0 +1,19 @@ +val moduleName by extra("dev.faststats.neoforge") + +plugins { + id("net.neoforged.moddev") version "2.0.141" +} + +neoForge { + version = "26.1.2.76" +} + +configurations.configureEach { + resolutionStrategy.force("com.google.code.gson:gson:2.13.2") +} + +dependencies { + api(project(":core")) + implementation(project(":config")) + compileOnly("net.neoforged:bus:8.0.5") +} diff --git a/neoforge/example-mod/build.gradle.kts b/neoforge/example-mod/build.gradle.kts new file mode 100644 index 00000000..725c766d --- /dev/null +++ b/neoforge/example-mod/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("net.neoforged.moddev") version "2.0.141" +} + +neoForge { + version = "26.1.2.76" +} + +configurations.configureEach { + resolutionStrategy.force("com.google.code.gson:gson:2.13.2") +} + +dependencies { + implementation(project(":neoforge")) +} + +tasks.jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(project(":config").sourceSets["main"].output) + from(project(":core").sourceSets["main"].output) + from(project(":neoforge").sourceSets["main"].output) +} diff --git a/neoforge/example-mod/src/main/java/com/example/ExampleMod.java b/neoforge/example-mod/src/main/java/com/example/ExampleMod.java new file mode 100644 index 00000000..4b93efa3 --- /dev/null +++ b/neoforge/example-mod/src/main/java/com/example/ExampleMod.java @@ -0,0 +1,30 @@ +package com.example; + +import dev.faststats.ErrorTracker; +import dev.faststats.data.Metric; +import dev.faststats.neoforge.NeoForgeContext; +import net.neoforged.fml.common.Mod; + +import java.util.concurrent.atomic.AtomicInteger; + +@Mod("example_mod") +public final class ExampleMod { + public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); + private final AtomicInteger gameCount = new AtomicInteger(); + + private final NeoForgeContext context = new NeoForgeContext.Factory( + "example_mod", + "YOUR_TOKEN_HERE" + ) + .metrics(factory -> factory + .addMetric(Metric.number("game_count", gameCount::get)) + .addMetric(Metric.string("server_version", () -> "1.0.0")) + .onFlush(() -> gameCount.set(0)) + .create()) + .errorTrackerService(ERROR_TRACKER) + .create(); + + public void startGame() { + gameCount.incrementAndGet(); + } +} diff --git a/neoforge/example-mod/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/example-mod/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..b69f9f56 --- /dev/null +++ b/neoforge/example-mod/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,10 @@ +modLoader = "javafml" +loaderVersion = "[1,)" +license = "MIT" + +[[mods]] +modId = "example_mod" +version = "1.0.0" +displayName = "Example Mod" +authors = "Your Name" +description = "Example FastStats NeoForge mod" diff --git a/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeContext.java b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeContext.java new file mode 100644 index 00000000..1dd61ca6 --- /dev/null +++ b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeContext.java @@ -0,0 +1,104 @@ +package dev.faststats.neoforge; + +import dev.faststats.Metrics; +import dev.faststats.SimpleContext; +import dev.faststats.SimpleMetrics; +import dev.faststats.Token; +import dev.faststats.config.SimpleConfig; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforgespi.language.IModInfo; +import org.jetbrains.annotations.Contract; + +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; + +/** + * NeoForge FastStats context. + * + * @since 0.26.2 + */ +public final class NeoForgeContext 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 IModInfo mod; + + private NeoForgeContext(final Factory factory, final String modId, @Token final String token) { + super(factory, SimpleConfig.read(FMLPaths.CONFIGDIR.get().resolve("faststats").resolve("config.properties")), "neoforge", token); + this.mod = ModList.get().getModContainerById(modId).map(container -> container.getModInfo()).orElseThrow(() -> { + return new IllegalArgumentException("Mod not found: " + modId); + }); + initializeServices(factory); + switch (FMLEnvironment.getDist()) { + case CLIENT -> ready(); + case DEDICATED_SERVER -> { + NeoForge.EVENT_BUS.addListener((final ServerStartedEvent event) -> ready()); + NeoForge.EVENT_BUS.addListener((final ServerStoppingEvent event) -> shutdown()); + } + } + } + + @Override + @Contract(value = " -> new", pure = true) + protected Metrics.Factory metricsFactory() { + return new SimpleMetrics.Factory(this) { + @Override + public Metrics create() throws IllegalStateException { + return switch (FMLEnvironment.getDist()) { + case CLIENT -> new NeoForgeMetricsClient(this, mod); + case DEDICATED_SERVER -> new NeoForgeMetricsServer(this, mod); + }; + } + }; + } + + @Override + protected boolean preSubmissionStart() { + return ((SimpleConfig) getConfig()).preSubmissionStart(getProjectName()); + } + + @Override + public String getProjectName() { + return mod.getModId(); + } + + @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() { + 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; + + public Factory(final String modId, @Token final String token) { + this.modId = modId; + this.token = token; + } + + @Override + public NeoForgeContext create() { + return new NeoForgeContext(this, modId, token); + } + } +} diff --git a/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetrics.java b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetrics.java new file mode 100644 index 00000000..50ebefa1 --- /dev/null +++ b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetrics.java @@ -0,0 +1,28 @@ +package dev.faststats.neoforge; + +import com.google.gson.JsonObject; +import dev.faststats.SimpleMetrics; +import net.neoforged.fml.ModList; +import net.neoforged.neoforgespi.language.IModInfo; + +abstract class NeoForgeMetrics extends SimpleMetrics { + protected final IModInfo mod; + + protected NeoForgeMetrics(final Factory factory, final IModInfo mod) throws IllegalStateException { + super(factory); + this.mod = mod; + } + + protected void appendNeoForgeData(final JsonObject metrics, final String serverType) { + metrics.addProperty("minecraft_version", modVersion("minecraft")); + metrics.addProperty("platform_version", modVersion("neoforge")); + metrics.addProperty("plugin_version", mod.getVersion().toString()); + metrics.addProperty("server_type", serverType); + } + + private static String modVersion(final String modId) { + return ModList.get().getModContainerById(modId) + .map(container -> container.getModInfo().getVersion().toString()) + .orElse("unknown"); + } +} diff --git a/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetricsClient.java b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetricsClient.java new file mode 100644 index 00000000..78c7fc1d --- /dev/null +++ b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetricsClient.java @@ -0,0 +1,29 @@ +package dev.faststats.neoforge; + +import com.google.gson.JsonObject; +import net.minecraft.client.Minecraft; +import net.neoforged.neoforgespi.language.IModInfo; + +final class NeoForgeMetricsClient extends NeoForgeMetrics { + NeoForgeMetricsClient(final Factory factory, final IModInfo mod) throws IllegalStateException { + super(factory, mod); + } + + @Override + protected void appendDefaultData(final JsonObject metrics) { + final var client = Minecraft.getInstance(); + metrics.addProperty("online_mode", client.getUser().getXuid().isPresent() && !client.isOfflineDeveloperMode()); + metrics.addProperty("player_count", getPlayerCount(client)); + appendNeoForgeData(metrics, "NeoForge Client"); + } + + private static int getPlayerCount(final Minecraft client) { + 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; + } +} diff --git a/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetricsServer.java b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetricsServer.java new file mode 100644 index 00000000..83ea2a1b --- /dev/null +++ b/neoforge/src/main/java/dev/faststats/neoforge/NeoForgeMetricsServer.java @@ -0,0 +1,25 @@ +package dev.faststats.neoforge; + +import com.google.gson.JsonObject; +import net.minecraft.server.MinecraftServer; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforgespi.language.IModInfo; +import org.jspecify.annotations.Nullable; + +final class NeoForgeMetricsServer extends NeoForgeMetrics { + private @Nullable MinecraftServer server; + + NeoForgeMetricsServer(final Factory factory, final IModInfo mod) throws IllegalStateException { + super(factory, mod); + NeoForge.EVENT_BUS.addListener((final ServerStartedEvent event) -> this.server = event.getServer()); + } + + @Override + protected void appendDefaultData(final JsonObject metrics) { + assert server != null : "Server not initialized"; + metrics.addProperty("online_mode", server.usesAuthentication()); + metrics.addProperty("player_count", server.getPlayerCount()); + appendNeoForgeData(metrics, "NeoForge"); + } +} diff --git a/neoforge/src/main/java/module-info.java b/neoforge/src/main/java/module-info.java new file mode 100644 index 00000000..96f97049 --- /dev/null +++ b/neoforge/src/main/java/module-info.java @@ -0,0 +1,15 @@ +import org.jspecify.annotations.NullMarked; + +@NullMarked +module dev.faststats.neoforge { + exports dev.faststats.neoforge; + + requires com.google.gson; + requires dev.faststats.config; + requires dev.faststats; + requires fml_loader; + requires net.neoforged.bus; + + requires static org.jetbrains.annotations; + requires static org.jspecify; +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8da8bdba..ea636283 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,6 @@ pluginManagement.repositories { maven("https://maven.fabricmc.net/") + maven("https://maven.neoforged.net/releases") gradlePluginPortal() } @@ -21,9 +22,11 @@ include("hytale") include("hytale:example-plugin") include("minestom") include("minestom:example-server") +include("neoforge") +include("neoforge:example-mod") include("nukkit") include("nukkit:example-plugin") include("sponge") include("sponge:example-plugin") include("velocity") -include("velocity:example-plugin") \ No newline at end of file +include("velocity:example-plugin")