diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 92dd490..8484bc9 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -93,9 +93,8 @@ Requires being logged in as `th0rgal` or having access to the `lfglabs` scope. - **Build**: `./gradlew build` - **Modules**: `core/` (shared logic), `bukkit/` (Paper/Spigot entry) - **Version**: Set in `plugin/gradle.properties` -- **PacketEvents**: Downloaded automatically at runtime via [Hopper](https://github.com/oraxen/hopper) +- **PacketEvents**: Downloaded and loaded automatically at runtime via [Hopper](https://github.com/oraxen/hopper) - no restart required - **Hopper**: Runtime dependency downloader (shaded into JAR, relocated) -- **Note**: First server start downloads PacketEvents and may require a restart ### API (`api/`) - **Database**: PostgreSQL (schema in `api/schema.sql`) diff --git a/README.md b/README.md index 0eb582b..72ace13 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,9 @@ AsyncAnticheat keeps detection off-server: the plugin captures packets and strea 1. Download the latest JAR from [Releases](https://github.com/oraxen/asyncanticheat/releases) 2. Place in your server's `plugins/` folder -3. Start the server - [PacketEvents](https://github.com/retrooper/packetevents) will be downloaded automatically via [Hopper](https://github.com/oraxen/hopper) -4. Restart if prompted (only on first install when PacketEvents is downloaded) -5. Configure `plugins/AsyncAnticheat/config.yml` with your API key -6. View detections at [asyncanticheat.com/dashboard](https://asyncanticheat.com/dashboard) +3. Start the server - [PacketEvents](https://github.com/retrooper/packetevents) will be downloaded and loaded automatically via [Hopper](https://github.com/oraxen/hopper) (no restart required!) +4. Configure `plugins/AsyncAnticheat/config.yml` with your API key +5. View detections at [asyncanticheat.com/dashboard](https://asyncanticheat.com/dashboard) Full setup guide: [asyncanticheat.com/docs](https://asyncanticheat.com/docs) diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 1aa4802..63e4a39 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -66,8 +66,8 @@ project(":bukkit") { // PacketEvents (compileOnly - downloaded at runtime via Hopper) compileOnly("com.github.retrooper:packetevents-spigot:2.7.0") - // Hopper - runtime dependency downloader (paper module includes bukkit) - implementation("md.thomas.hopper:hopper-paper:1.4.1") + // Hopper - runtime dependency downloader + implementation("md.thomas.hopper:hopper-bukkit:1.4.1") // bStats - plugin metrics implementation("org.bstats:bstats-bukkit:3.0.2") diff --git a/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AacBootstrap.java b/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AacBootstrap.java deleted file mode 100644 index e9e833c..0000000 --- a/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AacBootstrap.java +++ /dev/null @@ -1,41 +0,0 @@ -package md.thomas.asyncanticheat.bukkit; - -import io.papermc.paper.plugin.loader.PluginClasspathBuilder; -import io.papermc.paper.plugin.loader.PluginLoader; -import md.thomas.hopper.Dependency; -import md.thomas.hopper.DownloadResult; -import md.thomas.hopper.FailurePolicy; -import md.thomas.hopper.LogLevel; -import md.thomas.hopper.paper.HopperBootstrap; -import md.thomas.hopper.version.UpdatePolicy; -import org.jetbrains.annotations.NotNull; - -/** - * Paper plugin loader that downloads PacketEvents before the plugin class is loaded. - * This ensures PacketEvents classes are available when the plugin initializes. - * - * Configured in paper-plugin.yml with the loader property. - */ -@SuppressWarnings("UnstableApiUsage") -public class AacBootstrap implements PluginLoader { - - @Override - public void classloader(@NotNull PluginClasspathBuilder builder) { - if (Boolean.getBoolean("asyncanticheat.skipDependencyDownload")) { - return; - } - - DownloadResult result = HopperBootstrap.create(builder.getContext()) - .logLevel(LogLevel.NORMAL) - .require(Dependency.modrinth("packetevents") - .minVersion("2.7.0") - .updatePolicy(UpdatePolicy.MINOR) - .onFailure(FailurePolicy.FAIL) - .build()) - .download(); - - if (result.requiresRestart()) { - builder.getContext().getLogger().error("PacketEvents was downloaded. Please restart the server."); - } - } -} diff --git a/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AacHopper.java b/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AacHopper.java new file mode 100644 index 0000000..d6c0434 --- /dev/null +++ b/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AacHopper.java @@ -0,0 +1,133 @@ +package md.thomas.asyncanticheat.bukkit; + +import md.thomas.hopper.Dependency; +import md.thomas.hopper.FailurePolicy; +import md.thomas.hopper.LogLevel; +import md.thomas.hopper.bukkit.BukkitHopper; +import md.thomas.hopper.version.UpdatePolicy; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * Handles automatic downloading of PacketEvents using Hopper. + *
+ * Downloaded plugins are automatically loaded at runtime without requiring a server restart.
+ */
+public final class AacHopper {
+
+ private static boolean downloadComplete = false;
+ private static boolean requiresRestart = false;
+ private static boolean enabled = true;
+
+ // Matches: packetevents.jar, packetevents-spigot-2.7.0.jar, PacketEvents-2.7.0.jar
+ private static final Pattern PACKETEVENTS_PATTERN = Pattern.compile(
+ "(?i)^packetevents([-_][\\w.-]*)?\\.jar$"
+ );
+
+ private AacHopper() {}
+
+ /**
+ * Registers PacketEvents dependency with Hopper.
+ * Should be called in the plugin constructor.
+ *
+ * @param plugin the plugin instance
+ */
+ public static void register(@NotNull Plugin plugin) {
+ Logger logger = plugin.getLogger();
+
+ // Check if auto-download is disabled via system property (config not loaded in constructor)
+ if (Boolean.getBoolean("asyncanticheat.skipDependencyDownload")) {
+ enabled = false;
+ logger.info("Auto-download of dependencies is disabled");
+ return;
+ }
+
+ BukkitHopper.register(plugin, deps -> {
+ // Check if PacketEvents is already installed
+ boolean hasPacketEvents = pluginJarExists(PACKETEVENTS_PATTERN);
+
+ if (!hasPacketEvents) {
+ // Primary source: Modrinth (auto-detects platform)
+ deps.require(Dependency.modrinth("packetevents")
+ .name("PacketEvents")
+ .minVersion("2.7.0")
+ .updatePolicy(UpdatePolicy.MINOR)
+ .onFailure(FailurePolicy.FAIL)
+ .build());
+
+ // Fallback source: GitHub releases
+ deps.require(Dependency.github("retrooper/packetevents")
+ .name("PacketEvents")
+ .minVersion("2.7.0")
+ .assetPattern("*-spigot-*.jar")
+ .updatePolicy(UpdatePolicy.MINOR)
+ .onFailure(FailurePolicy.WARN_SKIP)
+ .build());
+ }
+ });
+ }
+
+ /**
+ * Downloads all registered dependencies and automatically loads them.
+ * Should be called in the plugin's onLoad() method.
+ *
+ * @param plugin the plugin instance
+ * @return true if all dependencies are satisfied and loaded
+ */
+ public static boolean download(@NotNull Plugin plugin) {
+ if (!enabled) {
+ downloadComplete = true;
+ return true;
+ }
+
+ Logger logger = plugin.getLogger();
+ BukkitHopper.DownloadAndLoadResult result = BukkitHopper.downloadAndLoad(plugin, LogLevel.QUIET);
+
+ downloadComplete = true;
+ requiresRestart = !result.noRestartRequired();
+
+ if (requiresRestart) {
+ logger.warning("Some dependencies require a server restart to load:");
+ for (var failed : result.loadResult().failed()) {
+ logger.warning(" - " + failed.path().getFileName() + ": " + failed.error());
+ }
+ }
+
+ return !requiresRestart;
+ }
+
+ /**
+ * @return true if a restart is required to load newly downloaded dependencies
+ */
+ public static boolean requiresRestart() {
+ return requiresRestart;
+ }
+
+ /**
+ * @return true if the download phase has completed
+ */
+ public static boolean isDownloadComplete() {
+ return downloadComplete;
+ }
+
+ /**
+ * Checks if a plugin jar file exists in the plugins folder using a regex pattern.
+ */
+ private static boolean pluginJarExists(Pattern pattern) {
+ File pluginsFolder = Bukkit.getPluginsFolder();
+ if (pluginsFolder == null || !pluginsFolder.exists()) {
+ return false;
+ }
+
+ File[] files = pluginsFolder.listFiles((dir, name) ->
+ pattern.matcher(name).matches()
+ );
+
+ return files != null && files.length > 0;
+ }
+}
diff --git a/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AsyncAnticheatBukkitPlugin.java b/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AsyncAnticheatBukkitPlugin.java
index 656bfe9..73ca509 100644
--- a/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AsyncAnticheatBukkitPlugin.java
+++ b/plugin/bukkit/src/main/java/md/thomas/asyncanticheat/bukkit/AsyncAnticheatBukkitPlugin.java
@@ -6,9 +6,6 @@
import md.thomas.asyncanticheat.core.AcLogger;
import md.thomas.asyncanticheat.core.AsyncAnticheatService;
import org.bstats.bukkit.Metrics;
-import org.bukkit.Bukkit;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@@ -16,47 +13,51 @@
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
-import java.util.List;
-
public final class AsyncAnticheatBukkitPlugin extends JavaPlugin {
private AsyncAnticheatService service;
private RecordingManager recordingManager;
private BukkitPlayerExemptionTracker exemptionTracker;
private SchedulerUtil.ScheduledTask stateTask;
- private Command registeredCommand;
private boolean packetEventsInitialized = false;
+ public AsyncAnticheatBukkitPlugin() {
+ // Register dependencies with Hopper for auto-download
+ AacHopper.register(this);
+ }
+
@Override
public void onLoad() {
- // PacketEvents is downloaded by AacBootstrap before this class loads (Paper)
- // or must be manually installed (Spigot)
+ // Download PacketEvents if needed (loads at runtime, no restart required)
+ AacHopper.download(this);
+
+ // Initialize PacketEvents
try {
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this));
PacketEvents.getAPI().load();
packetEventsInitialized = true;
} catch (Throwable t) {
- getLogger().severe("[AsyncAnticheat] Failed to load PacketEvents: " + t.getMessage());
- getLogger().severe("[AsyncAnticheat] PacketEvents is required. Install it from https://modrinth.com/plugin/packetevents");
+ getLogger().severe("Failed to load PacketEvents: " + t.getMessage());
+ getLogger().severe("PacketEvents is required. Install it from https://modrinth.com/plugin/packetevents");
}
}
@Override
public void onEnable() {
if (!packetEventsInitialized) {
- getLogger().severe("[AsyncAnticheat] PacketEvents not available. Disabling plugin.");
+ getLogger().severe("PacketEvents not available. Disabling plugin.");
getServer().getPluginManager().disablePlugin(this);
return;
}
final AcLogger logger = new BukkitLogger(getLogger());
service = new AsyncAnticheatService(getDataFolder(), logger);
-
+
// Initialize exemption tracker with config
// Tracks player states like creative mode, flying, dead, etc. based on NCP patterns
exemptionTracker = new BukkitPlayerExemptionTracker(service.getConfig().getExemptionConfig());
getServer().getPluginManager().registerEvents(exemptionTracker, this);
-
+
// Initialize PacketEvents (load was called in onLoad)
if (packetEventsInitialized) {
try {
@@ -66,14 +67,14 @@ public void onEnable() {
PacketListenerPriority.LOW
);
} catch (Throwable t) {
- logger.error("[AsyncAnticheat] Failed to initialize PacketEvents (Bukkit).", t);
+ logger.error("Failed to initialize PacketEvents.", t);
packetEventsInitialized = false;
}
}
// Initialize recording manager for in-game cheat recording
recordingManager = new RecordingManager(this, service.getConfig(), service.getServerId());
-
+
// Ensure recordings are stopped immediately on logout
getServer().getPluginManager().registerEvents(new Listener() {
@EventHandler
@@ -85,7 +86,6 @@ public void onQuit(PlayerQuitEvent event) {
}, this);
// Register main /aac command with subcommands
- // Use CommandMap directly for Paper plugin compatibility
registerCommand();
// Initialize bStats metrics (plugin ID: 20187)
@@ -131,11 +131,6 @@ public void onDisable() {
recordingManager.stopAll();
recordingManager = null;
}
- // Unregister command to prevent leak on reload
- if (registeredCommand != null) {
- registeredCommand.unregister(Bukkit.getCommandMap());
- registeredCommand = null;
- }
if (service != null) {
service.stop();
service = null;
@@ -155,32 +150,11 @@ public BukkitPlayerExemptionTracker getExemptionTracker() {
private void registerCommand() {
final BukkitMainCommand mainCmd = new BukkitMainCommand(service, recordingManager);
- // Try Spigot-style registration first (plugin.yml defines the command)
- // This works on Spigot servers where getCommand() returns the registered command
+ // Register command from plugin.yml
PluginCommand pluginCmd = getCommand("aac");
if (pluginCmd != null) {
pluginCmd.setExecutor(mainCmd);
pluginCmd.setTabCompleter(mainCmd);
- return;
}
-
- // Fall back to CommandMap registration for Paper plugins
- // Paper plugins use paper-plugin.yml which doesn't support getCommand()
- registeredCommand = new Command("aac", "AsyncAnticheat main command", "/aac [token|record|status]", List.of("asyncanticheat")) {
- @Override
- public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
- return mainCmd.onCommand(sender, this, commandLabel, args);
- }
-
- @Override
- public @NotNull List