diff --git a/README.md b/README.md
index afd40b1..dd6f204 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,29 @@
# DrawLib
-A simple library to draw some simple shapes using particles in Paper servers
+A simple library to draw some simple shapes using particles.

+## Compatibility
+
+A single compiled jar auto-detects what the running server supports at
+runtime and adapts accordingly -- there's nothing to configure.
+
+| Server software | Support |
+|---|---|
+| Paper / Paper forks (Purpur, Pufferfish, ...) | Full support: targeted, force-rendered particles |
+| Vanilla Bukkit / CraftBukkit / Spigot | Supported: per-player targeted particles (no "force show past render distance", since that's a Paper-only flag) |
+| Folia | Supported *if* you pass your `Plugin` instance to `new ShapeRenderer(plugin)` -- drawing is then automatically dispatched onto the correct region thread. Without a plugin instance, only safe if you already guarantee correct-thread calls yourself. |
+
+| Minecraft version | Support |
+|---|---|
+| 1.13 and newer | Full support, accurate arbitrary particle colour via `Particle.DustOptions` (handles the `REDSTONE` → `DUST` rename in 1.20.5 automatically) |
+| 1.9 - 1.12 | Supported via a legacy colour approximation (`Particle.DustOptions` doesn't exist yet on these versions) |
+| Below 1.9 | Not supported -- these versions predate Bukkit's `Particle` enum entirely |
+
+See `Compat.java` for the reflection-based detection this relies on.
+
## Maven
Add jitpack to your `repositories`:
@@ -30,7 +49,9 @@ For other tools, see the [JitPack](https://jitpack.io/#funnyboy-roks/DrawLib) pa
## Usage
```java
-ShapeRenderer renderer = new ShapeRenderer();
+// Pass your plugin instance so drawing works correctly on Folia too.
+// (new ShapeRenderer() with no arguments still works everywhere except Folia.)
+ShapeRenderer renderer = new ShapeRenderer(myPlugin);
renderer.setColor(Color.RED);
renderer.setStepSize(0.1);
diff --git a/pom.xml b/pom.xml
index cea53df..4f8bf86 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,13 +6,20 @@
* Intended Usage: *
- * ShapeRenderer renderer = new ShapeRenderer(); - * + * ShapeRenderer renderer = new ShapeRenderer(myPlugin); // pass your plugin for Folia support + * * renderer.setColor(Color.RED); * renderer.setStepSize(0.1); * renderer.setReceivers(player); @@ -27,16 +30,62 @@ ** * Note: The draw methods only show the particles once, it is recommended to draw them every tick. + * + *
+ * This is fine on every server platform except Folia -- if you + * might run on Folia, use {@link #ShapeRenderer(Plugin)} instead so + * particle calls are dispatched to the correct region thread. + */ public ShapeRenderer() { + this(null); + } + + /** + * @param plugin Your plugin instance. Required for correct behaviour on + * Folia (so drawing can be scheduled onto the right region + * thread); ignored/unused on every other server platform. + */ + public ShapeRenderer(@Nullable Plugin plugin) { + this.plugin = plugin; this.color = Color.RED; this.receivers = null; this.force = false; @@ -83,7 +132,9 @@ public void setReceivers(@NotNull Player... receivers) { } /** - * @param force If the particle should be forceshown to the players + * @param force If the particle should be forceshown to the players. Paper (and forks) only -- + * silently has no effect on vanilla Bukkit/Spigot, since that server software has + * no equivalent flag. */ public void setForceShow(boolean force) { this.force = force; @@ -95,21 +146,64 @@ public void setForceShow(boolean force) { * @param point The location to draw */ public void drawPoint(@NotNull Location point) { - point.getWorld().spawnParticle( - Particle.REDSTONE, - this.receivers, - null, - point.getX(), - point.getY(), - point.getZ(), - 1, - 0, - 0, - 0, - 0, - new Particle.DustOptions(this.color, .5f), - this.force - ); + Compat.runRegionAware(this.plugin, point, () -> this.spawnParticleAt(point)); + } + + /** + * Does the actual particle spawning, choosing the best API the running + * server supports. Always invoked on a thread that's safe to touch + * {@code point}'s world (either the calling thread on non-Folia servers, + * or the correct region thread on Folia). + */ + private void spawnParticleAt(@NotNull Location point) { + World world = point.getWorld(); + if (world == null || Compat.DUST_PARTICLE == null) { + return; + } + + if (Compat.HAS_DUST_OPTIONS) { + Particle.DustOptions data = new Particle.DustOptions(this.color, .5f); + + if (Compat.hasPaperParticleApi()) { + // Best case: Paper / Paper fork -- exact receivers + force-render support + Compat.spawnParticlePaper( + world, Compat.DUST_PARTICLE, this.receivers, + point.getX(), point.getY(), point.getZ(), data, this.force + ); + return; + } + + // Vanilla Bukkit/CraftBukkit/Spigot: no receivers/force overload, + // so target players manually (or broadcast if none were set). + if (this.receivers == null) { + world.spawnParticle(Compat.DUST_PARTICLE, point, 1, 0, 0, 0, 0, data); + } else { + for (Player p : this.receivers) { + p.spawnParticle(Compat.DUST_PARTICLE, point, 1, 0, 0, 0, 0, data); + } + } + return; + } + + // Pre-1.13 fallback: Particle.DustOptions doesn't exist yet. Fake + // the colour using the legacy "count = 0, offsets = RGB" redstone + // dust trick instead. + this.spawnLegacyColouredParticle(world, point); + } + + private void spawnLegacyColouredParticle(@NotNull World world, @NotNull Location point) { + // Red must be > 0 or the client treats the particle as invisible/default-coloured. + double r = Math.max(this.color.getRed() / 255d, 0.0001d); + double g = this.color.getGreen() / 255d; + double b = this.color.getBlue() / 255d; + + if (this.receivers == null) { + world.spawnParticle(Compat.DUST_PARTICLE, point, 0, r, g, b, 1); + } else { + for (Player p : this.receivers) { + p.spawnParticle(Compat.DUST_PARTICLE, point, 0, r, g, b, 1); + } + } } /** @@ -313,8 +407,3 @@ public void drawBlockFace(@NotNull Block block, @NotNull BlockFace face) { drawCuboid(pt1, pt2); } } - - - - - diff --git a/src/main/java/com/funnyboyroks/drawlib/util/Compat.java b/src/main/java/com/funnyboyroks/drawlib/util/Compat.java new file mode 100644 index 0000000..8d838a1 --- /dev/null +++ b/src/main/java/com/funnyboyroks/drawlib/util/Compat.java @@ -0,0 +1,194 @@ +package com.funnyboyroks.drawlib.util; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Runtime feature-detection so that a single compiled DrawLib jar can run + * as-is across the widest practical range of server software and Minecraft + * versions, degrading gracefully instead of hard-failing when an API isn't + * present: + *
+ * Every check below is resolved once via reflection and cached in a static
+ * field. None of these lookups throw if the class/method in question doesn't
+ * exist on the running server -- they simply report "not available" so the
+ * caller can fall back to an older/simpler code path.
+ */
+public final class Compat {
+
+ private static final Logger LOG = Logger.getLogger("DrawLib");
+
+ private Compat() {
+ }
+
+ // ---------------------------------------------------------------
+ // Folia detection
+ // ---------------------------------------------------------------
+
+ /** True if this server is running Folia (regionised multithreading). */
+ public static final boolean IS_FOLIA = classExists("io.papermc.paper.threadedregions.RegionizedServer");
+
+ private static final Method GET_REGION_SCHEDULER = IS_FOLIA
+ ? findMethod(Bukkit.class, "getRegionScheduler")
+ : null;
+
+ private static final Method REGION_SCHEDULER_EXECUTE = IS_FOLIA
+ ? findRegionSchedulerExecute()
+ : null;
+
+ // ---------------------------------------------------------------
+ // Paper-only expanded spawnParticle(receivers, source, ..., force)
+ // ---------------------------------------------------------------
+
+ private static final Method PAPER_SPAWN_PARTICLE = findPaperSpawnParticle();
+
+ /** True if the Paper-only targeted/force-render spawnParticle overload is available. */
+ public static boolean hasPaperParticleApi() {
+ return PAPER_SPAWN_PARTICLE != null;
+ }
+
+ // ---------------------------------------------------------------
+ // Particle.DustOptions (added 1.13)
+ // ---------------------------------------------------------------
+
+ /** True if {@code Particle.DustOptions} exists on this server (MC 1.13+). */
+ public static final boolean HAS_DUST_OPTIONS = classExists("org.bukkit.Particle$DustOptions");
+
+ // ---------------------------------------------------------------
+ // Particle enum constant: DUST (1.20.5+) or REDSTONE (older)
+ // ---------------------------------------------------------------
+
+ /**
+ * The correct enum constant for a colourable dust particle on this
+ * server, or {@code null} if neither name resolves (should not happen
+ * on any real Bukkit-family server that has a Particle enum at all).
+ */
+ public static final Particle DUST_PARTICLE = resolveDustParticle();
+
+ // ---------------------------------------------------------------
+ // Detection helpers
+ // ---------------------------------------------------------------
+
+ private static boolean classExists(String name) {
+ try {
+ Class.forName(name);
+ return true;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ private static Method findMethod(Class> clazz, String name, Class>... params) {
+ try {
+ return clazz.getMethod(name, params);
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ private static Method findPaperSpawnParticle() {
+ try {
+ return org.bukkit.World.class.getMethod(
+ "spawnParticle",
+ Particle.class, List.class, Player.class,
+ double.class, double.class, double.class,
+ int.class, double.class, double.class, double.class,
+ double.class, Object.class, boolean.class
+ );
+ } catch (Throwable t) {
+ return null;
+ }
+ }
+
+ private static Method findRegionSchedulerExecute() {
+ try {
+ Class> schedulerClass = Class.forName("io.papermc.paper.threadedregions.scheduler.RegionScheduler");
+ for (Method m : schedulerClass.getMethods()) {
+ // execute(Plugin, Location, Runnable)
+ if (m.getName().equals("execute") && m.getParameterCount() == 3) {
+ return m;
+ }
+ }
+ } catch (Throwable ignored) {
+ // Not on Folia, or API shape changed -- caller falls back to running inline.
+ }
+ return null;
+ }
+
+ private static Particle resolveDustParticle() {
+ try {
+ return Particle.valueOf("DUST");
+ } catch (Throwable t) {
+ try {
+ return Particle.valueOf("REDSTONE");
+ } catch (Throwable t2) {
+ return null;
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------
+ // Invocation helpers
+ // ---------------------------------------------------------------
+
+ /**
+ * Calls Paper's expanded {@code spawnParticle(receivers, source, ..., force)}
+ * overload via reflection. Only call this after checking {@link #hasPaperParticleApi()}.
+ */
+ public static