aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--settings.gradle9
-rw-r--r--spark-neoforge/build.gradle64
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeClassSourceLookup.java36
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeCommandSender.java77
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeExtraMetadataProvider.java73
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlatformInfo.java53
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlayerPingProvider.java45
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeServerConfigProvider.java56
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeSparkMod.java80
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickHook.java73
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickReporter.java72
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeWorldInfoProvider.java172
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeClientSparkPlugin.java153
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeServerSparkPlugin.java266
-rw-r--r--spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeSparkPlugin.java169
-rw-r--r--spark-neoforge/src/main/resources/META-INF/accesstransformer.cfg7
-rw-r--r--spark-neoforge/src/main/resources/META-INF/mods.toml20
-rw-r--r--spark-neoforge/src/main/resources/pack.mcmeta6
18 files changed, 1431 insertions, 0 deletions
diff --git a/settings.gradle b/settings.gradle
index ffa36cd..aa7e7f3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -8,10 +8,18 @@ pluginManagement {
name = 'Forge'
url = 'https://maven.minecraftforge.net/'
}
+ maven {
+ name = 'NeoForge'
+ url = 'https://maven.neoforged.net/releases'
+ }
gradlePluginPortal()
}
}
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0'
+}
+
rootProject.name = 'spark'
include (
'spark-api',
@@ -23,6 +31,7 @@ include (
'spark-sponge7',
'spark-sponge8',
'spark-forge',
+ 'spark-neoforge',
'spark-fabric',
'spark-nukkit',
'spark-waterdog',
diff --git a/spark-neoforge/build.gradle b/spark-neoforge/build.gradle
new file mode 100644
index 0000000..177449f
--- /dev/null
+++ b/spark-neoforge/build.gradle
@@ -0,0 +1,64 @@
+plugins {
+ id 'com.github.johnrengelman.shadow' version '8.1.1'
+ id 'net.neoforged.gradle.userdev' version '7.0.97'
+}
+
+java.toolchain.languageVersion = JavaLanguageVersion.of(17)
+
+tasks.withType(JavaCompile) {
+ // override, compile targeting J17
+ options.release = 17
+}
+
+minecraft {
+ accessTransformers {
+ file('src/main/resources/META-INF/accesstransformer.cfg')
+ }
+}
+
+configurations {
+ shade
+ implementation.extendsFrom shade
+}
+
+dependencies {
+ implementation "net.neoforged:neoforge:20.4.223"
+ shade project(':spark-common')
+}
+
+processResources {
+ from(sourceSets.main.resources.srcDirs) {
+ include 'META-INF/mods.toml'
+ expand (
+ 'pluginVersion': project.pluginVersion,
+ 'pluginDescription': project.pluginDescription
+ )
+ }
+
+ from(sourceSets.main.resources.srcDirs) {
+ exclude 'META-INF/mods.toml'
+ }
+}
+
+shadowJar {
+ archiveFileName = "spark-${project.pluginVersion}-neoforge.jar"
+ configurations = [project.configurations.shade]
+
+ relocate 'net.kyori.adventure', 'me.lucko.spark.lib.adventure'
+ relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination'
+ relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy'
+ relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf'
+ relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm'
+ relocate 'one.profiler', 'me.lucko.spark.lib.asyncprofiler'
+ relocate 'me.lucko.bytesocks.client', 'me.lucko.spark.lib.bytesocks'
+ relocate 'org.java_websocket', 'me.lucko.spark.lib.bytesocks.ws'
+
+ exclude 'module-info.class'
+ exclude 'META-INF/maven/**'
+ exclude 'META-INF/proguard/**'
+}
+
+artifacts {
+ archives shadowJar
+ shadow shadowJar
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeClassSourceLookup.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeClassSourceLookup.java
new file mode 100644
index 0000000..5e60ee7
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeClassSourceLookup.java
@@ -0,0 +1,36 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import cpw.mods.modlauncher.TransformingClassLoader;
+import me.lucko.spark.common.sampler.source.ClassSourceLookup;
+
+public class NeoForgeClassSourceLookup implements ClassSourceLookup {
+
+ @Override
+ public String identify(Class<?> clazz) {
+ if (clazz.getClassLoader() instanceof TransformingClassLoader) {
+ String name = clazz.getModule().getName();
+ return name.equals("forge") || name.equals("minecraft") ? null : name;
+ }
+ return null;
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeCommandSender.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeCommandSender.java
new file mode 100644
index 0000000..2f6a411
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeCommandSender.java
@@ -0,0 +1,77 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import me.lucko.spark.common.command.sender.AbstractCommandSender;
+import me.lucko.spark.neoforge.plugin.NeoForgeSparkPlugin;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.minecraft.commands.CommandSource;
+import net.minecraft.network.chat.Component.Serializer;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.rcon.RconConsoleSource;
+import net.minecraft.world.entity.player.Player;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public class NeoForgeCommandSender extends AbstractCommandSender<CommandSource> {
+ private final NeoForgeSparkPlugin plugin;
+
+ public NeoForgeCommandSender(CommandSource source, NeoForgeSparkPlugin plugin) {
+ super(source);
+ this.plugin = plugin;
+ }
+
+ @Override
+ public String getName() {
+ if (super.delegate instanceof Player) {
+ return ((Player) super.delegate).getGameProfile().getName();
+ } else if (super.delegate instanceof MinecraftServer) {
+ return "Console";
+ } else if (super.delegate instanceof RconConsoleSource) {
+ return "RCON Console";
+ } else {
+ return "unknown:" + super.delegate.getClass().getSimpleName();
+ }
+ }
+
+ @Override
+ public UUID getUniqueId() {
+ if (super.delegate instanceof Player) {
+ return ((Player) super.delegate).getUUID();
+ }
+ return null;
+ }
+
+ @Override
+ public void sendMessage(Component message) {
+ MutableComponent component = Serializer.fromJson(GsonComponentSerializer.gson().serializeToTree(message));
+ Objects.requireNonNull(component, "component");
+ super.delegate.sendSystemMessage(component);
+ }
+
+ @Override
+ public boolean hasPermission(String permission) {
+ return this.plugin.hasPermission(super.delegate, permission);
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeExtraMetadataProvider.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeExtraMetadataProvider.java
new file mode 100644
index 0000000..e18b3d3
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeExtraMetadataProvider.java
@@ -0,0 +1,73 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import me.lucko.spark.common.platform.MetadataProvider;
+import net.minecraft.server.packs.repository.Pack;
+import net.minecraft.server.packs.repository.PackRepository;
+import net.minecraft.server.packs.repository.PackSource;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class NeoForgeExtraMetadataProvider implements MetadataProvider {
+
+ private final PackRepository resourcePackManager;
+
+ public NeoForgeExtraMetadataProvider(PackRepository resourcePackManager) {
+ this.resourcePackManager = resourcePackManager;
+ }
+
+ @Override
+ public Map<String, JsonElement> get() {
+ Map<String, JsonElement> metadata = new LinkedHashMap<>();
+ metadata.put("datapacks", datapackMetadata());
+ return metadata;
+ }
+
+ private JsonElement datapackMetadata() {
+ JsonObject datapacks = new JsonObject();
+ for (Pack profile : this.resourcePackManager.getSelectedPacks()) {
+ JsonObject obj = new JsonObject();
+ obj.addProperty("name", profile.getTitle().getString());
+ obj.addProperty("description", profile.getDescription().getString());
+ obj.addProperty("source", resourcePackSource(profile.getPackSource()));
+ datapacks.add(profile.getId(), obj);
+ }
+ return datapacks;
+ }
+
+ private static String resourcePackSource(PackSource source) {
+ if (source == PackSource.DEFAULT) {
+ return "none";
+ } else if (source == PackSource.BUILT_IN) {
+ return "builtin";
+ } else if (source == PackSource.WORLD) {
+ return "world";
+ } else if (source == PackSource.SERVER) {
+ return "server";
+ } else {
+ return "unknown";
+ }
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlatformInfo.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlatformInfo.java
new file mode 100644
index 0000000..ea0141e
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlatformInfo.java
@@ -0,0 +1,53 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import me.lucko.spark.common.platform.PlatformInfo;
+import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion;
+import net.neoforged.neoforge.internal.versions.neoform.NeoFormVersion;
+
+public class NeoForgePlatformInfo implements PlatformInfo {
+ private final Type type;
+
+ public NeoForgePlatformInfo(Type type) {
+ this.type = type;
+ }
+
+ @Override
+ public Type getType() {
+ return this.type;
+ }
+
+ @Override
+ public String getName() {
+ return "NeoForge";
+ }
+
+ @Override
+ public String getVersion() {
+ return NeoForgeVersion.getVersion();
+ }
+
+ @Override
+ public String getMinecraftVersion() {
+ return NeoFormVersion.getMCVersion();
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlayerPingProvider.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlayerPingProvider.java
new file mode 100644
index 0000000..191b60e
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgePlayerPingProvider.java
@@ -0,0 +1,45 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import com.google.common.collect.ImmutableMap;
+import me.lucko.spark.common.monitor.ping.PlayerPingProvider;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+
+import java.util.Map;
+
+public class NeoForgePlayerPingProvider implements PlayerPingProvider {
+ private final MinecraftServer server;
+
+ public NeoForgePlayerPingProvider(MinecraftServer server) {
+ this.server = server;
+ }
+
+ @Override
+ public Map<String, Integer> poll() {
+ ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
+ for (ServerPlayer player : this.server.getPlayerList().getPlayers()) {
+ builder.put(player.getGameProfile().getName(), player.connection.latency());
+ }
+ return builder.build();
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeServerConfigProvider.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeServerConfigProvider.java
new file mode 100644
index 0000000..813683d
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeServerConfigProvider.java
@@ -0,0 +1,56 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import me.lucko.spark.common.platform.serverconfig.ConfigParser;
+import me.lucko.spark.common.platform.serverconfig.PropertiesConfigParser;
+import me.lucko.spark.common.platform.serverconfig.ServerConfigProvider;
+
+import java.util.Collection;
+import java.util.Map;
+
+public class NeoForgeServerConfigProvider extends ServerConfigProvider {
+
+ /** A map of provided files and their type */
+ private static final Map<String, ConfigParser> FILES;
+ /** A collection of paths to be excluded from the files */
+ private static final Collection<String> HIDDEN_PATHS;
+
+ public NeoForgeServerConfigProvider() {
+ super(FILES, HIDDEN_PATHS);
+ }
+
+ static {
+ ImmutableSet.Builder<String> hiddenPaths = ImmutableSet.<String>builder()
+ .add("server-ip")
+ .add("motd")
+ .add("resource-pack")
+ .add("rcon<dot>password")
+ .add("level-seed")
+ .addAll(getSystemPropertyList("spark.serverconfigs.hiddenpaths"));
+
+ FILES = ImmutableMap.of("server.properties", PropertiesConfigParser.INSTANCE);
+ HIDDEN_PATHS = hiddenPaths.build();
+ }
+
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeSparkMod.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeSparkMod.java
new file mode 100644
index 0000000..16c19ec
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeSparkMod.java
@@ -0,0 +1,80 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import me.lucko.spark.neoforge.plugin.NeoForgeClientSparkPlugin;
+import me.lucko.spark.neoforge.plugin.NeoForgeServerSparkPlugin;
+import net.neoforged.bus.api.IEventBus;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.fml.IExtensionPoint;
+import net.neoforged.fml.ModContainer;
+import net.neoforged.fml.ModLoadingContext;
+import net.neoforged.fml.common.Mod;
+import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
+import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
+import net.neoforged.fml.loading.FMLPaths;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.server.ServerAboutToStartEvent;
+
+import java.nio.file.Path;
+
+@Mod("spark")
+public class NeoForgeSparkMod {
+
+ private ModContainer container;
+ private Path configDirectory;
+
+ private IEventBus eventBus;
+
+ public NeoForgeSparkMod(IEventBus eventBus) {
+ this.eventBus = eventBus;
+ eventBus.addListener(this::setup);
+ eventBus.addListener(this::clientInit);
+ NeoForge.EVENT_BUS.register(this);
+
+ ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> IExtensionPoint.DisplayTest.IGNORESERVERONLY, (a, b) -> true));
+ }
+
+ public String getVersion() {
+ return this.container.getModInfo().getVersion().toString();
+ }
+
+ public void setup(FMLCommonSetupEvent e) {
+ this.container = ModLoadingContext.get().getActiveContainer();
+ this.configDirectory = FMLPaths.CONFIGDIR.get().resolve(this.container.getModId());
+ }
+
+ public void clientInit(FMLClientSetupEvent e) {
+ NeoForgeClientSparkPlugin.register(this, e);
+ }
+
+ @SubscribeEvent
+ public void serverInit(ServerAboutToStartEvent e) {
+ NeoForgeServerSparkPlugin.register(this, e);
+ }
+
+ public Path getConfigDirectory() {
+ if (this.configDirectory == null) {
+ throw new IllegalStateException("Config directory not set");
+ }
+ return this.configDirectory;
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickHook.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickHook.java
new file mode 100644
index 0000000..84e1aff
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickHook.java
@@ -0,0 +1,73 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import me.lucko.spark.common.tick.AbstractTickHook;
+import me.lucko.spark.common.tick.TickHook;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.TickEvent;
+
+public class NeoForgeTickHook extends AbstractTickHook implements TickHook {
+ private final TickEvent.Type type;
+
+ public NeoForgeTickHook(TickEvent.Type type) {
+ this.type = type;
+ }
+
+ @SubscribeEvent
+ public void onTick(TickEvent.ServerTickEvent e) {
+ if (e.phase != TickEvent.Phase.START) {
+ return;
+ }
+
+ if (e.type != this.type) {
+ return;
+ }
+
+ onTick();
+ }
+
+ @SubscribeEvent
+ public void onTick(TickEvent.ClientTickEvent e) {
+ if (e.phase != TickEvent.Phase.START) {
+ return;
+ }
+
+ if (e.type != this.type) {
+ return;
+ }
+
+ onTick();
+ }
+
+
+ @Override
+ public void start() {
+ NeoForge.EVENT_BUS.register(this);
+ }
+
+ @Override
+ public void close() {
+ NeoForge.EVENT_BUS.unregister(this);
+ }
+
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickReporter.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickReporter.java
new file mode 100644
index 0000000..be61d3e
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeTickReporter.java
@@ -0,0 +1,72 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import me.lucko.spark.common.tick.SimpleTickReporter;
+import me.lucko.spark.common.tick.TickReporter;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.TickEvent;
+
+public class NeoForgeTickReporter extends SimpleTickReporter implements TickReporter {
+ private final TickEvent.Type type;
+
+ public NeoForgeTickReporter(TickEvent.Type type) {
+ this.type = type;
+ }
+
+ @SubscribeEvent
+ public void onTick(TickEvent.ServerTickEvent e) {
+ if (e.type != this.type) {
+ return;
+ }
+
+ switch (e.phase) {
+ case START -> onStart();
+ case END -> onEnd();
+ default -> throw new AssertionError(e.phase);
+ }
+ }
+ @SubscribeEvent
+ public void onTick(TickEvent.ClientTickEvent e) {
+ if (e.type != this.type) {
+ return;
+ }
+
+ switch (e.phase) {
+ case START -> onStart();
+ case END -> onEnd();
+ default -> throw new AssertionError(e.phase);
+ }
+ }
+
+ @Override
+ public void start() {
+ NeoForge.EVENT_BUS.register(this);
+ }
+
+ @Override
+ public void close() {
+ NeoForge.EVENT_BUS.unregister(this);
+ super.close();
+ }
+
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeWorldInfoProvider.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeWorldInfoProvider.java
new file mode 100644
index 0000000..ef76646
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/NeoForgeWorldInfoProvider.java
@@ -0,0 +1,172 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge;
+
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongSet;
+import me.lucko.spark.common.platform.world.AbstractChunkInfo;
+import me.lucko.spark.common.platform.world.CountMap;
+import me.lucko.spark.common.platform.world.WorldInfoProvider;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.entity.EntityLookup;
+import net.minecraft.world.level.entity.EntitySection;
+import net.minecraft.world.level.entity.EntitySectionStorage;
+import net.minecraft.world.level.entity.PersistentEntitySectionManager;
+import net.minecraft.world.level.entity.TransientEntitySectionManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Stream;
+
+public abstract class NeoForgeWorldInfoProvider implements WorldInfoProvider {
+
+ protected List<ForgeChunkInfo> getChunksFromCache(EntitySectionStorage<Entity> cache) {
+ LongSet loadedChunks = cache.getAllChunksWithExistingSections();
+ List<ForgeChunkInfo> list = new ArrayList<>(loadedChunks.size());
+
+ for (LongIterator iterator = loadedChunks.iterator(); iterator.hasNext(); ) {
+ long chunkPos = iterator.nextLong();
+ Stream<EntitySection<Entity>> sections = cache.getExistingSectionsInChunk(chunkPos);
+
+ list.add(new ForgeChunkInfo(chunkPos, sections));
+ }
+
+ return list;
+ }
+
+ public static final class Server extends NeoForgeWorldInfoProvider {
+ private final MinecraftServer server;
+
+ public Server(MinecraftServer server) {
+ this.server = server;
+ }
+
+ @Override
+ public CountsResult pollCounts() {
+ int players = this.server.getPlayerCount();
+ int entities = 0;
+ int chunks = 0;
+
+ for (ServerLevel level : this.server.getAllLevels()) {
+ PersistentEntitySectionManager<Entity> entityManager = level.entityManager;
+ EntityLookup<Entity> entityIndex = entityManager.visibleEntityStorage;
+
+ entities += entityIndex.count();
+ chunks += level.getChunkSource().getLoadedChunksCount();
+ }
+
+ return new CountsResult(players, entities, -1, chunks);
+ }
+
+ @Override
+ public ChunksResult<ForgeChunkInfo> pollChunks() {
+ ChunksResult<ForgeChunkInfo> data = new ChunksResult<>();
+
+ for (ServerLevel level : this.server.getAllLevels()) {
+ PersistentEntitySectionManager<Entity> entityManager = level.entityManager;
+ EntitySectionStorage<Entity> cache = entityManager.sectionStorage;
+
+ List<ForgeChunkInfo> list = getChunksFromCache(cache);
+ data.put(level.dimension().location().getPath(), list);
+ }
+
+ return data;
+ }
+ }
+
+ public static final class Client extends NeoForgeWorldInfoProvider {
+ private final Minecraft client;
+
+ public Client(Minecraft client) {
+ this.client = client;
+ }
+
+ @Override
+ public CountsResult pollCounts() {
+ ClientLevel level = this.client.level;
+ if (level == null) {
+ return null;
+ }
+
+ TransientEntitySectionManager<Entity> entityManager = level.entityStorage;
+ EntityLookup<Entity> entityIndex = entityManager.entityStorage;
+
+ int entities = entityIndex.count();
+ int chunks = level.getChunkSource().getLoadedChunksCount();
+
+ return new CountsResult(-1, entities, -1, chunks);
+ }
+
+ @Override
+ public ChunksResult<ForgeChunkInfo> pollChunks() {
+ ChunksResult<ForgeChunkInfo> data = new ChunksResult<>();
+
+ ClientLevel level = this.client.level;
+ if (level == null) {
+ return null;
+ }
+
+ TransientEntitySectionManager<Entity> entityManager = level.entityStorage;
+ EntitySectionStorage<Entity> cache = entityManager.sectionStorage;
+
+ List<ForgeChunkInfo> list = getChunksFromCache(cache);
+ data.put(level.dimension().location().getPath(), list);
+
+ return data;
+ }
+ }
+
+ public static final class ForgeChunkInfo extends AbstractChunkInfo<EntityType<?>> {
+ private final CountMap<EntityType<?>> entityCounts;
+
+ ForgeChunkInfo(long chunkPos, Stream<EntitySection<Entity>> entities) {
+ super(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos));
+
+ this.entityCounts = new CountMap.Simple<>(new HashMap<>());
+ entities.forEach(section -> {
+ if (section.getStatus().isAccessible()) {
+ section.getEntities().forEach(entity ->
+ this.entityCounts.increment(entity.getType())
+ );
+ }
+ });
+ }
+
+ @Override
+ public CountMap<EntityType<?>> getEntityCounts() {
+ return this.entityCounts;
+ }
+
+ @Override
+ public String entityTypeName(EntityType<?> type) {
+ return EntityType.getKey(type).toString();
+ }
+ }
+
+
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeClientSparkPlugin.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeClientSparkPlugin.java
new file mode 100644
index 0000000..92bd656
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeClientSparkPlugin.java
@@ -0,0 +1,153 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge.plugin;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import me.lucko.spark.common.platform.MetadataProvider;
+import me.lucko.spark.common.platform.PlatformInfo;
+import me.lucko.spark.common.platform.world.WorldInfoProvider;
+import me.lucko.spark.common.sampler.ThreadDumper;
+import me.lucko.spark.common.tick.TickHook;
+import me.lucko.spark.common.tick.TickReporter;
+import me.lucko.spark.neoforge.NeoForgeCommandSender;
+import me.lucko.spark.neoforge.NeoForgeExtraMetadataProvider;
+import me.lucko.spark.neoforge.NeoForgePlatformInfo;
+import me.lucko.spark.neoforge.NeoForgeSparkMod;
+import me.lucko.spark.neoforge.NeoForgeTickHook;
+import me.lucko.spark.neoforge.NeoForgeTickReporter;
+import me.lucko.spark.neoforge.NeoForgeWorldInfoProvider;
+import net.minecraft.client.Minecraft;
+import net.minecraft.commands.CommandSource;
+import net.minecraft.commands.CommandSourceStack;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
+import net.neoforged.neoforge.client.event.RegisterClientCommandsEvent;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.TickEvent;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+
+public class NeoForgeClientSparkPlugin extends NeoForgeSparkPlugin implements Command<CommandSourceStack>, SuggestionProvider<CommandSourceStack> {
+
+ public static void register(NeoForgeSparkMod mod, FMLClientSetupEvent event) {
+ NeoForgeClientSparkPlugin plugin = new NeoForgeClientSparkPlugin(mod, Minecraft.getInstance());
+ plugin.enable();
+ }
+
+ private final Minecraft minecraft;
+ private final ThreadDumper gameThreadDumper;
+
+ public NeoForgeClientSparkPlugin(NeoForgeSparkMod mod, Minecraft minecraft) {
+ super(mod);
+ this.minecraft = minecraft;
+ this.gameThreadDumper = new ThreadDumper.Specific(minecraft.gameThread);
+ }
+
+ @Override
+ public void enable() {
+ super.enable();
+
+ // register listeners
+ NeoForge.EVENT_BUS.register(this);
+ }
+
+ @SubscribeEvent
+ public void onCommandRegister(RegisterClientCommandsEvent e) {
+ registerCommands(e.getDispatcher(), this, this, "sparkc", "sparkclient");
+ }
+
+ @Override
+ public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
+ String[] args = processArgs(context, false, "sparkc", "sparkclient");
+ if (args == null) {
+ return 0;
+ }
+
+ this.platform.executeCommand(new NeoForgeCommandSender(context.getSource().getEntity(), this), args);
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) throws CommandSyntaxException {
+ String[] args = processArgs(context, true, "/sparkc", "/sparkclient");
+ if (args == null) {
+ return Suggestions.empty();
+ }
+
+ return generateSuggestions(new NeoForgeCommandSender(context.getSource().getEntity(), this), args, builder);
+ }
+
+ @Override
+ public boolean hasPermission(CommandSource sender, String permission) {
+ return true;
+ }
+
+ @Override
+ public Stream<NeoForgeCommandSender> getCommandSenders() {
+ return Stream.of(new NeoForgeCommandSender(this.minecraft.player, this));
+ }
+
+ @Override
+ public void executeSync(Runnable task) {
+ this.minecraft.executeIfPossible(task);
+ }
+
+ @Override
+ public ThreadDumper getDefaultThreadDumper() {
+ return this.gameThreadDumper;
+ }
+
+ @Override
+ public TickHook createTickHook() {
+ return new NeoForgeTickHook(TickEvent.Type.CLIENT);
+ }
+
+ @Override
+ public TickReporter createTickReporter() {
+ return new NeoForgeTickReporter(TickEvent.Type.CLIENT);
+ }
+
+ @Override
+ public WorldInfoProvider createWorldInfoProvider() {
+ return new NeoForgeWorldInfoProvider.Client(this.minecraft);
+ }
+
+ @Override
+ public MetadataProvider createExtraMetadataProvider() {
+ return new NeoForgeExtraMetadataProvider(this.minecraft.getResourcePackRepository());
+ }
+
+ @Override
+ public PlatformInfo getPlatformInfo() {
+ return new NeoForgePlatformInfo(PlatformInfo.Type.CLIENT);
+ }
+
+ @Override
+ public String getCommandName() {
+ return "sparkc";
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeServerSparkPlugin.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeServerSparkPlugin.java
new file mode 100644
index 0000000..94522c1
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeServerSparkPlugin.java
@@ -0,0 +1,266 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge.plugin;
+
+import com.google.common.collect.ImmutableMap;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import me.lucko.spark.common.monitor.ping.PlayerPingProvider;
+import me.lucko.spark.common.platform.MetadataProvider;
+import me.lucko.spark.common.platform.PlatformInfo;
+import me.lucko.spark.common.platform.serverconfig.ServerConfigProvider;
+import me.lucko.spark.common.platform.world.WorldInfoProvider;
+import me.lucko.spark.common.sampler.ThreadDumper;
+import me.lucko.spark.common.tick.TickHook;
+import me.lucko.spark.common.tick.TickReporter;
+import me.lucko.spark.neoforge.NeoForgeCommandSender;
+import me.lucko.spark.neoforge.NeoForgeExtraMetadataProvider;
+import me.lucko.spark.neoforge.NeoForgePlatformInfo;
+import me.lucko.spark.neoforge.NeoForgePlayerPingProvider;
+import me.lucko.spark.neoforge.NeoForgeServerConfigProvider;
+import me.lucko.spark.neoforge.NeoForgeSparkMod;
+import me.lucko.spark.neoforge.NeoForgeTickHook;
+import me.lucko.spark.neoforge.NeoForgeTickReporter;
+import me.lucko.spark.neoforge.NeoForgeWorldInfoProvider;
+import net.minecraft.commands.CommandSource;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.event.RegisterCommandsEvent;
+import net.neoforged.neoforge.event.TickEvent;
+import net.neoforged.neoforge.event.server.ServerAboutToStartEvent;
+import net.neoforged.neoforge.event.server.ServerStoppingEvent;
+import net.neoforged.neoforge.server.permission.PermissionAPI;
+import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
+import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
+import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class NeoForgeServerSparkPlugin extends NeoForgeSparkPlugin implements Command<CommandSourceStack>, SuggestionProvider<CommandSourceStack> {
+
+ public static void register(NeoForgeSparkMod mod, ServerAboutToStartEvent event) {
+ NeoForgeServerSparkPlugin plugin = new NeoForgeServerSparkPlugin(mod, event.getServer());
+ plugin.enable();
+ }
+
+ private static final PermissionNode.PermissionResolver<Boolean> DEFAULT_PERMISSION_VALUE = (player, playerUUID, context) -> {
+ if (player == null) {
+ return false;
+ }
+
+ MinecraftServer server = player.getServer();
+ if (server != null && server.isSingleplayerOwner(player.getGameProfile())) {
+ return true;
+ }
+
+ return player.hasPermissions(4);
+ };
+
+ private final MinecraftServer server;
+ private final ThreadDumper gameThreadDumper;
+ private Map<String, PermissionNode<Boolean>> registeredPermissions = Collections.emptyMap();
+
+ public NeoForgeServerSparkPlugin(NeoForgeSparkMod mod, MinecraftServer server) {
+ super(mod);
+ this.server = server;
+ this.gameThreadDumper = new ThreadDumper.Specific(server.getRunningThread());
+ }
+
+ @Override
+ public void enable() {
+ super.enable();
+
+ // register commands
+ registerCommands(this.server.getCommands().getDispatcher());
+
+ // register listeners
+ NeoForge.EVENT_BUS.register(this);
+ }
+
+ @Override
+ public void disable() {
+ super.disable();
+
+ // unregister listeners
+ NeoForge.EVENT_BUS.unregister(this);
+ }
+
+ @SubscribeEvent
+ public void onDisable(ServerStoppingEvent event) {
+ if (event.getServer() == this.server) {
+ disable();
+ }
+ }
+
+ @SubscribeEvent
+ public void onPermissionGather(PermissionGatherEvent.Nodes e) {
+ // collect all possible permissions
+ List<String> permissions = this.platform.getCommands().stream()
+ .map(me.lucko.spark.common.command.Command::primaryAlias)
+ .collect(Collectors.toList());
+
+ // special case for the "spark" permission: map it to "spark.all"
+ permissions.add("all");
+
+ // register permissions with forge & keep a copy for lookup
+ ImmutableMap.Builder<String, PermissionNode<Boolean>> builder = ImmutableMap.builder();
+
+ Map<String, PermissionNode<?>> alreadyRegistered = e.getNodes().stream().collect(Collectors.toMap(PermissionNode::getNodeName, Function.identity()));
+
+ for (String permission : permissions) {
+ String permissionString = "spark." + permission;
+
+ // there's a weird bug where it seems that this listener can be called twice, causing an
+ // IllegalArgumentException to be thrown the second time e.addNodes is called.
+ PermissionNode<?> existing = alreadyRegistered.get(permissionString);
+ if (existing != null) {
+ //noinspection unchecked
+ builder.put(permissionString, (PermissionNode<Boolean>) existing);
+ continue;
+ }
+
+ PermissionNode<Boolean> node = new PermissionNode<>("spark", permission, PermissionTypes.BOOLEAN, DEFAULT_PERMISSION_VALUE);
+ e.addNodes(node);
+ builder.put(permissionString, node);
+ }
+ this.registeredPermissions = builder.build();
+ }
+
+ @SubscribeEvent
+ public void onCommandRegister(RegisterCommandsEvent e) {
+ registerCommands(e.getDispatcher());
+ }
+
+ private void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
+ registerCommands(dispatcher, this, this, "spark");
+ }
+
+ @Override
+ public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
+ String[] args = processArgs(context, false, "/spark", "spark");
+ if (args == null) {
+ return 0;
+ }
+
+ CommandSource source = context.getSource().getEntity() != null ? context.getSource().getEntity() : context.getSource().getServer();
+ this.platform.executeCommand(new NeoForgeCommandSender(source, this), args);
+ return Command.SINGLE_SUCCESS;
+ }
+
+ @Override
+ public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) throws CommandSyntaxException {
+ String[] args = processArgs(context, true, "/spark", "spark");
+ if (args == null) {
+ return Suggestions.empty();
+ }
+
+ return generateSuggestions(new NeoForgeCommandSender(context.getSource().getPlayerOrException(), this), args, builder);
+ }
+
+ @Override
+ public boolean hasPermission(CommandSource sender, String permission) {
+ if (sender instanceof ServerPlayer) {
+ if (permission.equals("spark")) {
+ permission = "spark.all";
+ }
+
+ PermissionNode<Boolean> permissionNode = this.registeredPermissions.get(permission);
+ if (permissionNode == null) {
+ throw new IllegalStateException("spark permission not registered: " + permission);
+ }
+ return PermissionAPI.getPermission((ServerPlayer) sender, permissionNode);
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public Stream<NeoForgeCommandSender> getCommandSenders() {
+ return Stream.concat(
+ this.server.getPlayerList().getPlayers().stream(),
+ Stream.of(this.server)
+ ).map(sender -> new NeoForgeCommandSender(sender, this));
+ }
+
+ @Override
+ public void executeSync(Runnable task) {
+ this.server.executeIfPossible(task);
+ }
+
+ @Override
+ public ThreadDumper getDefaultThreadDumper() {
+ return this.gameThreadDumper;
+ }
+
+ @Override
+ public TickHook createTickHook() {
+ return new NeoForgeTickHook(TickEvent.Type.SERVER);
+ }
+
+ @Override
+ public TickReporter createTickReporter() {
+ return new NeoForgeTickReporter(TickEvent.Type.SERVER);
+ }
+
+ @Override
+ public PlayerPingProvider createPlayerPingProvider() {
+ return new NeoForgePlayerPingProvider(this.server);
+ }
+
+ @Override
+ public ServerConfigProvider createServerConfigProvider() {
+ return new NeoForgeServerConfigProvider();
+ }
+
+ @Override
+ public MetadataProvider createExtraMetadataProvider() {
+ return new NeoForgeExtraMetadataProvider(this.server.getPackRepository());
+ }
+
+ @Override
+ public WorldInfoProvider createWorldInfoProvider() {
+ return new NeoForgeWorldInfoProvider.Server(this.server);
+ }
+
+ @Override
+ public PlatformInfo getPlatformInfo() {
+ return new NeoForgePlatformInfo(PlatformInfo.Type.SERVER);
+ }
+
+ @Override
+ public String getCommandName() {
+ return "spark";
+ }
+}
diff --git a/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeSparkPlugin.java b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeSparkPlugin.java
new file mode 100644
index 0000000..43d77a1
--- /dev/null
+++ b/spark-neoforge/src/main/java/me/lucko/spark/neoforge/plugin/NeoForgeSparkPlugin.java
@@ -0,0 +1,169 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.neoforge.plugin;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import me.lucko.spark.common.SparkPlatform;
+import me.lucko.spark.common.SparkPlugin;
+import me.lucko.spark.common.command.sender.CommandSender;
+import me.lucko.spark.common.sampler.source.ClassSourceLookup;
+import me.lucko.spark.common.sampler.source.SourceMetadata;
+import me.lucko.spark.common.util.SparkThreadFactory;
+import me.lucko.spark.neoforge.NeoForgeClassSourceLookup;
+import me.lucko.spark.neoforge.NeoForgeSparkMod;
+import net.minecraft.commands.CommandSource;
+import net.neoforged.fml.ModList;
+import net.neoforged.neoforgespi.language.IModInfo;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.logging.Level;
+
+public abstract class NeoForgeSparkPlugin implements SparkPlugin {
+
+ private final NeoForgeSparkMod mod;
+ private final Logger logger;
+ protected final ScheduledExecutorService scheduler;
+
+ protected SparkPlatform platform;
+
+ protected NeoForgeSparkPlugin(NeoForgeSparkMod mod) {
+ this.mod = mod;
+ this.logger = LogManager.getLogger("spark");
+ this.scheduler = Executors.newScheduledThreadPool(4, new SparkThreadFactory());
+ }
+
+ public void enable() {
+ this.platform = new SparkPlatform(this);
+ this.platform.enable();
+ }
+
+ public void disable() {
+ this.platform.disable();
+ this.scheduler.shutdown();
+ }
+
+ public abstract boolean hasPermission(CommandSource sender, String permission);
+
+ @Override
+ public String getVersion() {
+ return this.mod.getVersion();
+ }
+
+ @Override
+ public Path getPluginDirectory() {
+ return this.mod.getConfigDirectory();
+ }
+
+ @Override
+ public void executeAsync(Runnable task) {
+ this.scheduler.execute(task);
+ }
+
+ @Override
+ public void log(Level level, String msg) {
+ if (level == Level.INFO) {
+ this.logger.info(msg);
+ } else if (level == Level.WARNING) {
+ this.logger.warn(msg);
+ } else if (level == Level.SEVERE) {
+ this.logger.error(msg);
+ } else {
+ throw new IllegalArgumentException(level.getName());
+ }
+ }
+
+ @Override
+ public ClassSourceLookup createClassSourceLookup() {
+ return new NeoForgeClassSourceLookup();
+ }
+
+ @Override
+ public Collection<SourceMetadata> getKnownSources() {
+ return SourceMetadata.gather(
+ ModList.get().getMods(),
+ IModInfo::getModId,
+ mod -> mod.getVersion().toString(),
+ mod -> null // ?
+ );
+ }
+
+ protected CompletableFuture<Suggestions> generateSuggestions(CommandSender sender, String[] args, SuggestionsBuilder builder) {
+ SuggestionsBuilder suggestions;
+
+ int lastSpaceIdx = builder.getRemaining().lastIndexOf(' ');
+ if (lastSpaceIdx != -1) {
+ suggestions = builder.createOffset(builder.getStart() + lastSpaceIdx + 1);
+ } else {
+ suggestions = builder;
+ }
+
+ return CompletableFuture.supplyAsync(() -> {
+ for (String suggestion : this.platform.tabCompleteCommand(sender, args)) {
+ suggestions.suggest(suggestion);
+ }
+ return suggestions.build();
+ });
+ }
+
+ protected static <T> void registerCommands(CommandDispatcher<T> dispatcher, Command<T> executor, SuggestionProvider<T> suggestor, String... aliases) {
+ if (aliases.length == 0) {
+ return;
+ }
+
+ String mainName = aliases[0];
+ LiteralArgumentBuilder<T> command = LiteralArgumentBuilder.<T>literal(mainName)
+ .executes(executor)
+ .then(RequiredArgumentBuilder.<T, String>argument("args", StringArgumentType.greedyString())
+ .suggests(suggestor)
+ .executes(executor)
+ );
+
+ LiteralCommandNode<T> node = dispatcher.register(command);
+ for (int i = 1; i < aliases.length; i++) {
+ dispatcher.register(LiteralArgumentBuilder.<T>literal(aliases[i]).redirect(node));
+ }
+ }
+
+ protected static String[] processArgs(CommandContext<?> context, boolean tabComplete, String... aliases) {
+ String[] split = context.getInput().split(" ", tabComplete ? -1 : 0);
+ if (split.length == 0 || !Arrays.asList(aliases).contains(split[0])) {
+ return null;
+ }
+
+ return Arrays.copyOfRange(split, 1, split.length);
+ }
+}
diff --git a/spark-neoforge/src/main/resources/META-INF/accesstransformer.cfg b/spark-neoforge/src/main/resources/META-INF/accesstransformer.cfg
new file mode 100644
index 0000000..43d14fc
--- /dev/null
+++ b/spark-neoforge/src/main/resources/META-INF/accesstransformer.cfg
@@ -0,0 +1,7 @@
+public net.minecraft.server.level.ServerLevel entityManager # entityManager
+public net.minecraft.world.level.entity.PersistentEntitySectionManager sectionStorage # sectionStorage
+public net.minecraft.world.level.entity.PersistentEntitySectionManager visibleEntityStorage # visibleEntityStorage
+public net.minecraft.client.multiplayer.ClientLevel entityStorage # entityStorage
+public net.minecraft.world.level.entity.TransientEntitySectionManager sectionStorage # sectionStorage
+public net.minecraft.world.level.entity.TransientEntitySectionManager entityStorage # entityStorage
+public net.minecraft.client.Minecraft gameThread # gameThread
diff --git a/spark-neoforge/src/main/resources/META-INF/mods.toml b/spark-neoforge/src/main/resources/META-INF/mods.toml
new file mode 100644
index 0000000..f44574d
--- /dev/null
+++ b/spark-neoforge/src/main/resources/META-INF/mods.toml
@@ -0,0 +1,20 @@
+modLoader="javafml"
+loaderVersion="[2,)"
+authors="Luck"
+license="GPLv3"
+
+[[mods]]
+modId="spark"
+displayName="spark"
+version="${pluginVersion}"
+description="${pluginDescription}"
+
+[[accessTransformers]]
+file="META-INF/accesstransformer.cfg"
+
+[[dependencies.spark]]
+modId = "neoforge"
+mandatory=true
+versionRange = "[20,)"
+ordering = "NONE"
+side = "BOTH" \ No newline at end of file
diff --git a/spark-neoforge/src/main/resources/pack.mcmeta b/spark-neoforge/src/main/resources/pack.mcmeta
new file mode 100644
index 0000000..d34a5b7
--- /dev/null
+++ b/spark-neoforge/src/main/resources/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "spark resources",
+ "pack_format": 6
+ }
+}