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 tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) { - List result = mainCmd.onTabComplete(sender, this, alias, args); - return result != null ? result : List.of(); - } - }; - - Bukkit.getCommandMap().register("asyncanticheat", registeredCommand); } } - - diff --git a/plugin/bukkit/src/main/resources/paper-plugin.yml b/plugin/bukkit/src/main/resources/paper-plugin.yml deleted file mode 100644 index f8ac1aa..0000000 --- a/plugin/bukkit/src/main/resources/paper-plugin.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: AsyncAnticheat -version: ${version} -main: md.thomas.asyncanticheat.bukkit.AsyncAnticheatBukkitPlugin -loader: md.thomas.asyncanticheat.bukkit.AacBootstrap -api-version: "1.21" -folia-supported: true -description: Async anticheat packet capture and streaming via PacketEvents -authors: - - th0rgal - -dependencies: - server: - packetevents: - load: BEFORE - required: true - join-classpath: true - ProtocolLib: - load: BEFORE - required: false - ProtocolSupport: - load: BEFORE - required: false - ViaVersion: - load: BEFORE - required: false - ViaBackwards: - load: BEFORE - required: false - ViaRewind: - load: BEFORE - required: false - Geyser-Spigot: - load: BEFORE - required: false diff --git a/plugin/gradle.properties b/plugin/gradle.properties index 00a0a4c..45da49f 100644 --- a/plugin/gradle.properties +++ b/plugin/gradle.properties @@ -1,3 +1,3 @@ -pluginVersion=0.3.0 +pluginVersion=0.4.1 diff --git a/web/content/installation/index.mdx b/web/content/installation/index.mdx index d631505..7a7c3fa 100644 --- a/web/content/installation/index.mdx +++ b/web/content/installation/index.mdx @@ -30,7 +30,7 @@ Or browse all versions on the [GitHub releases](https://github.com/oraxen/asynca 1. Drop the JAR file into your server's `/plugins/` folder 2. Start your server -3. **First-time only:** The plugin will automatically download [PacketEvents](https://github.com/retrooper/packetevents) via [Hopper](https://github.com/oraxen/hopper). If prompted, restart the server once. +3. **First-time only:** The plugin will automatically download and load [PacketEvents](https://github.com/retrooper/packetevents) via [Hopper](https://github.com/oraxen/hopper). No restart required! That's it! The plugin will automatically generate a default configuration. diff --git a/web/public/thread/screenshots/modules.webp b/web/public/thread/screenshots/modules.webp new file mode 100644 index 0000000..b26c4c7 Binary files /dev/null and b/web/public/thread/screenshots/modules.webp differ diff --git a/web/public/thread/screenshots/overview.webp b/web/public/thread/screenshots/overview.webp new file mode 100644 index 0000000..f00bb64 Binary files /dev/null and b/web/public/thread/screenshots/overview.webp differ diff --git a/web/public/thread/screenshots/players.webp b/web/public/thread/screenshots/players.webp new file mode 100644 index 0000000..35d9481 Binary files /dev/null and b/web/public/thread/screenshots/players.webp differ