Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

![Screenshot](./img/screenshot.png)

## 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`:
Expand All @@ -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);
Expand Down
58 changes: 53 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@

<groupId>com.funnyboyroks</groupId>
<artifactId>DrawLib</artifactId>
<version>1.1.0-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>DrawLib</name>

<properties>
<java.version>11</java.version>
<!--
Deliberately low: many old MC server versions (and the JREs
people still run them on) top out around Java 8. Nothing in
this codebase needs a newer language level, and Java's class
file format is forward-compatible, so this jar still runs fine
on modern JREs too.
-->
<java.version>8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Expand Down Expand Up @@ -60,6 +67,10 @@
</distributionManagement>

<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
Expand All @@ -71,10 +82,47 @@
</repositories>

<dependencies>
<!--
Compile against plain Spigot-API rather than paper-api.

This is deliberate: Paper's expanded spawnParticle(receivers,
source, ..., force) overload and Folia's scheduler classes are
NOT part of Spigot-API, so compiling against paper-api would
silently bake in a hard dependency on Paper being present at
runtime. Instead, ShapeRenderer/Compat reach for those
Paper/Folia-only APIs via reflection (see Compat.java) and fall
back to plain Spigot-API behaviour when they aren't there. That
means the exact same built jar loads and works on vanilla
Bukkit/CraftBukkit/Spigot, Paper, Paper forks, and Folia alike.

1.13 is chosen as the floor because it's the oldest version with
Particle.DustOptions (colourable dust particles); older servers
(1.9-1.12) are still supported at runtime via a legacy colour
trick in ShapeRenderer, they just aren't what we compile against.
-->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.13-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>

<!--
Compile-time only annotations used throughout the codebase
(@NotNull/@Contract from JetBrains, @Nullable from JSR305).
Scoped "provided" since these aren't needed at runtime and
shouldn't be bundled into the shaded jar.
-->
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.19.2-R0.1-SNAPSHOT</version>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
Expand Down
137 changes: 113 additions & 24 deletions src/main/java/com/funnyboyroks/drawlib/renderer/ShapeRenderer.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.funnyboyroks.drawlib.renderer;

import com.funnyboyroks.drawlib.util.Compat;
import com.funnyboyroks.drawlib.util.Util;
import com.funnyboyroks.drawlib.util.Vector;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nullable;
Expand All @@ -18,25 +21,71 @@
* <p>
* Intended Usage:
* <pre>
* 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);
* renderer.drawLine(point1, point2);
* </pre>
*
* <em>Note: The draw methods only show the particles once, it is recommended to draw them every tick.</em>
*
* <h2>Compatibility</h2>
* ShapeRenderer detects, at runtime, what the underlying server actually
* supports and adapts automatically -- a single compiled jar works across:
* <ul>
* <li><b>Paper / Paper forks</b> (Purpur, Pufferfish, ...): uses Paper's
* targeted, force-rendered {@code spawnParticle} overload -- identical
* behaviour to the original DrawLib.</li>
* <li><b>Vanilla Bukkit / CraftBukkit / Spigot</b>: falls back to
* per-player {@link Player#spawnParticle}, or a world-wide particle if
* no receivers were set. There is no "force show past render distance"
* on these platforms, since that flag is a Paper-only addition.</li>
* <li><b>Folia</b>: if a {@link Plugin} instance is supplied via the
* constructor, every particle spawn is dispatched onto the correct
* region thread automatically. Without a plugin instance, calls run on
* whatever thread invoked them, which is only safe on non-Folia
* servers.</li>
* <li><b>Minecraft 1.13+</b>: uses {@link Particle.DustOptions} for
* accurate, arbitrary particle colour (handles the {@code REDSTONE} to
* {@code DUST} rename in 1.20.5 automatically).</li>
* <li><b>Minecraft 1.9 - 1.12</b>: {@code Particle.DustOptions} doesn't
* exist yet, so colour is approximated with the legacy "offsets as
* RGB, count = 0" redstone-particle trick.</li>
* </ul>
* Minecraft versions older than 1.9 aren't supported, since those predate
* Bukkit's {@code Particle} enum entirely (a fundamentally different,
* string/id-based effect API would be required).
*/
public class ShapeRenderer {

private final Plugin plugin; // Used only to schedule on the correct thread on Folia; may be null

private Color color; // Colour of the particles to be drawn
private List<Player> receivers; // Players that will receive the particles
private boolean force; // Force show particles
private boolean force; // Force show particles (Paper only; ignored elsewhere)
private double step_size = .1; // Step between two particles
private boolean optimize; // Optimize particles

/**
* Create a renderer with no owning plugin.
* <p>
* This is fine on every server platform <em>except</em> 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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
}
}

/**
Expand Down Expand Up @@ -313,8 +407,3 @@ public void drawBlockFace(@NotNull Block block, @NotNull BlockFace face) {
drawCuboid(pt1, pt2);
}
}





Loading