diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ae7f0bc..73d3e33 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -81,7 +81,7 @@ jobs:
VERSION: ${{ matrix.version }}
run: |
if [ $VERSION == "1.19.4" ]; then
- url="https://ci.dmulloy2.net/job/ProtocolLib/lastStableBuild/artifact/target/ProtocolLib.jar"
+ url="https://ci.dmulloy2.net/job/ProtocolLib/lastSuccessfulBuild/artifact/build/libs/ProtocolLib.jar"
else
url="https://github.com/dmulloy2/ProtocolLib/releases/download/4.8.0/ProtocolLib.jar"
fi
diff --git a/README.md b/README.md
index 805c1e1..cb1877a 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[![bStats Servers](https://img.shields.io/bstats/servers/10243)](https://bstats.org/plugin/bukkit/Yamipa/10243)
[![License](https://img.shields.io/github/license/josemmo/yamipa)](LICENSE)
-Yamipa is an Spigot plugin that allows players to place images (even **animated**!) on any surface in your Minecraft server
+Yamipa is a Spigot plugin that allows players to place images (even **animated**!) on any surface in your Minecraft server
without having to install any local client mod.
It is designed with performance and compatibility in mind, so even the most low-specs servers should be able to run it.
@@ -24,8 +24,9 @@ Download the JAR file for the [latest release](https://github.com/josemmo/yamipa
### Requirements
Before installing Yamipa make sure you meet the following requirements:
-- CraftBukkit, Spigot or PaperMC v1.16 or higher
-- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) v4.6.1 or higher
+- CraftBukkit, Spigot or PaperMC 1.16 or higher
+- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) v4.8.0 or higher
+ (latest [dev build](https://ci.dmulloy2.net/job/ProtocolLib/lastSuccessfulBuild/) for 1.19.4)
Here are the Minecraft distributions where Yamipa should be able to run:
| Minecraft version | CraftBukkit | Spigot | PaperMC |
@@ -132,6 +133,7 @@ The supported plugins are:
- [WorldGuard](https://enginehub.org/worldguard/)
- [GriefPrevention](https://www.spigotmc.org/resources/griefprevention.1884/)
+- [Towny Advanced](https://townyadvanced.github.io/)
## Flags
Images from this plugin have a set of boolean attributes called "flags" that modify its behavior. Possible values are:
diff --git a/pom.xml b/pom.xml
index acedb80..b88b449 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.josemmo.bukkit.plugin
YamipaPlugin
- 1.2.9
+ 1.2.10
8
@@ -59,7 +59,7 @@
org.bstats
bstats-bukkit
- 3.0.1
+ 3.0.2
com.sk89q.worldguard
@@ -73,6 +73,12 @@
16.18.1
provided
+
+ com.github.TownyAdvanced
+ towny
+ 0.98.6.25
+ provided
+
org.jetbrains
annotations
diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java
index 73c712e..bdbd13d 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommand.java
@@ -212,7 +212,7 @@ public static boolean placeImage(
// Make sure image can be placed
for (Location loc : fakeImage.getAllLocations()) {
- if (!Permissions.canEditBlock(player, loc)) {
+ if (!Permissions.canBuild(player, loc)) {
ActionBar.send(player, ChatColor.RED + "You're not allowed to place an image here!");
return false;
}
@@ -260,7 +260,7 @@ public static void removeImage(@NotNull Player player) {
public static boolean removeImage(@NotNull Player player, @NotNull FakeImage image) {
// Check block permissions
for (Location loc : image.getAllLocations()) {
- if (!Permissions.canEditBlock(player, loc)) {
+ if (!Permissions.canDestroy(player, loc)) {
ActionBar.send(player, ChatColor.RED + "You're not allowed to remove this image!");
return false;
}
@@ -299,7 +299,7 @@ public static void clearImages(
Player senderAsPlayer = (Player) sender;
images.removeIf(image -> {
for (Location loc : image.getAllLocations()) {
- if (!Permissions.canEditBlock(senderAsPlayer, loc)) {
+ if (!Permissions.canDestroy(senderAsPlayer, loc)) {
return true;
}
}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java
index 789c37a..419e524 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeEntity.java
@@ -1,11 +1,13 @@
package io.josemmo.bukkit.plugin.renderer;
+import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import io.josemmo.bukkit.plugin.YamipaPlugin;
+import io.josemmo.bukkit.plugin.utils.Internals;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
@@ -94,6 +96,23 @@ protected static void tryToSendPacket(@NotNull Player player, @NotNull PacketCon
}
}
+ /**
+ * Try to send several packets
+ * @param player Player who will receive the packets
+ * @param packets Packets to send
+ */
+ protected static void tryToSendPackets(@NotNull Player player, @NotNull Iterable packets) {
+ if (Internals.MINECRAFT_VERSION < 19.4f) {
+ for (PacketContainer packet : packets) {
+ tryToSendPacket(player, packet);
+ }
+ } else {
+ PacketContainer container = new PacketContainer(PacketType.Play.Server.BUNDLE);
+ container.getPacketBundles().write(0, packets);
+ tryToSendPacket(player, container);
+ }
+ }
+
/**
* Try to run asynchronous task
* @param callback Callback to execute
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java
index 20000fb..7740de8 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java
@@ -1,5 +1,6 @@
package io.josemmo.bukkit.plugin.renderer;
+import com.comphenix.protocol.events.PacketContainer;
import io.josemmo.bukkit.plugin.storage.ImageFile;
import io.josemmo.bukkit.plugin.utils.DirectionUtils;
import org.bukkit.Location;
@@ -12,6 +13,7 @@
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.*;
+import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
@@ -429,11 +431,19 @@ public void spawn(@NotNull Player player) {
* @param player Player instance
*/
private void spawnOnceLoaded(@NotNull Player player) {
+ String playerName = player.getName();
observingPlayers.add(player);
+
+ // Prepare packets to send
+ List packets = new ArrayList<>();
for (FakeItemFrame frame : frames) {
- frame.spawn(player);
- frame.render(player, 0);
+ packets.add(frame.getSpawnPacket());
+ packets.addAll(frame.getRenderPackets(player, 0));
+ plugin.fine("Spawned FakeItemFrame#" + frame.getId() + " for Player#" + playerName);
}
+
+ // Send packets
+ tryToSendPackets(player, packets);
}
/**
@@ -459,9 +469,13 @@ public void destroy(@Nullable Player player) {
if (frames != null) {
Set targets = (player == null) ? observingPlayers : Collections.singleton(player);
for (Player target : targets) {
+ String targetName = target.getName();
+ List packets = new ArrayList<>();
for (FakeItemFrame frame : frames) {
- frame.destroy(target);
+ packets.add(frame.getDestroyPacket());
+ plugin.fine("Destroyed FakeItemFrame#" + frame.getId() + " for Player#" + targetName);
}
+ tryToSendPackets(target, packets);
}
}
@@ -521,9 +535,11 @@ private void nextStep() {
currentStep = (currentStep + 1) % numOfSteps;
try {
for (Player player : observingPlayers) {
+ List packets = new ArrayList<>();
for (FakeItemFrame frame : frames) {
- frame.render(player, currentStep);
+ packets.addAll(frame.getRenderPackets(player, currentStep));
}
+ tryToSendPackets(player, packets);
}
} catch (ConcurrentModificationException e) {
// We can safely ignore this exception as it will just result
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java
index 2c352fb..0a6f4ec 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java
@@ -1,5 +1,6 @@
package io.josemmo.bukkit.plugin.renderer;
+import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
@@ -15,6 +16,8 @@
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class FakeItemFrame extends FakeEntity {
@@ -67,10 +70,18 @@ public FakeItemFrame(
}
/**
- * Spawn empty item frame in player's client
- * @param player Player instance
+ * Get frame ID
+ * @return Frame ID
*/
- public void spawn(@NotNull Player player) {
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Get entity spawn packet
+ * @return Spawn packet
+ */
+ public @NotNull SpawnEntityPacket getSpawnPacket() {
// Calculate frame position in relation to target block
double x = location.getBlockX();
double y = location.getBlockY();
@@ -115,18 +126,23 @@ public void spawn(@NotNull Player player) {
.setPosition(x, y, z)
.setRotation(pitch, yaw)
.setData(orientation);
- tryToSendPacket(player, framePacket);
- plugin.fine("Spawned FakeItemFrame#" + this.id + " for Player#" + player.getName());
+
+ return framePacket;
}
/**
- * Send frame of animation to player
- * @param player Player instance
- * @param step Map step to send
+ * Get frame of animation packets
+ * @param player Player who is expected to receive packets (for caching reasons)
+ * @param step Map step
*/
- public void render(@NotNull Player player, int step) {
- // Send map pixels
- maps[step].sendPixels(player);
+ public @NotNull List getRenderPackets(@NotNull Player player, int step) {
+ List packets = new ArrayList<>(2);
+
+ // Enqueue map pixels packet (if needed)
+ boolean mustSendPixels = maps[step].requestResend(player);
+ if (mustSendPixels) {
+ packets.add(maps[step].getPixelsPacket());
+ }
// Create and attach filled map
ItemStack itemStack = MinecraftReflection.getBukkitItemStack(new ItemStack(Material.FILLED_MAP));
@@ -135,25 +151,24 @@ public void render(@NotNull Player player, int step) {
NbtFactory.setItemTag(itemStack, itemStackNbt);
// Build entity metadata packet
- EntityMetadataPacket mapPacket = new EntityMetadataPacket();
- mapPacket.setId(id)
+ EntityMetadataPacket metadataPacket = new EntityMetadataPacket();
+ metadataPacket.setId(id)
.setInvisible(true)
.setItem(itemStack)
.setRotation(rotation)
.build();
+ packets.add(metadataPacket);
- // Send animation status update
- tryToSendPacket(player, mapPacket);
+ return packets;
}
/**
- * Destroy item frame from player's client
- * @param player Player instance
+ * Get destroy item frame packet
+ * @return Destroy packet
*/
- public void destroy(@NotNull Player player) {
+ public @NotNull DestroyEntityPacket getDestroyPacket() {
DestroyEntityPacket destroyPacket = new DestroyEntityPacket();
destroyPacket.setId(id);
- tryToSendPacket(player, destroyPacket);
- plugin.fine("Destroyed FakeItemFrame#" + this.id + " for Player#" + player.getName());
+ return destroyPacket;
}
}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java
index fa00be7..a063bb8 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeMap.java
@@ -120,30 +120,37 @@ public byte[] getPixels() {
}
/**
- * Send map pixels to player
- * @param player Player instance
+ * Request re-send of map pixels
+ * @param player Player who is expected to receive pixels
+ * @return Whether re-send authorization was granted or not
*/
- public void sendPixels(@NotNull Player player) {
+ public boolean requestResend(@NotNull Player player) {
UUID uuid = player.getUniqueId();
long now = Instant.now().getEpochSecond();
- // Avoid re-sending pixels too frequently
+ // Has enough time passed since last re-send?
long last = lastPlayerSendTime.getOrDefault(uuid, 0L);
if ((now-last) <= RESEND_THRESHOLD && (player.getLastPlayed()/1000) < last) {
- return;
+ return false;
}
- // Create map data packet
+ // Authorize re-send and update latest timestamp
+ lastPlayerSendTime.put(uuid, now);
+ plugin.fine("Granted sending pixels for FakeMap#" + id + " to Player#" + player.getName());
+ return true;
+ }
+
+ /**
+ * Get map pixels packet
+ * @return Map pixels packet
+ */
+ public @NotNull MapDataPacket getPixelsPacket() {
MapDataPacket mapDataPacket = new MapDataPacket();
mapDataPacket.setId(id)
.setScale(0) // Fully zoomed-in
.setLocked(true)
.setArea(DIMENSION, DIMENSION, 0, 0)
.setPixels(pixels);
-
- // Send packet
- tryToSendPacket(player, mapDataPacket);
- lastPlayerSendTime.put(uuid, now);
- plugin.fine("Sent pixels for FakeMap#" + id + " to Player#" + player.getName());
+ return mapDataPacket;
}
}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java
index 52a59c2..e1c1586 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/utils/Permissions.java
@@ -1,14 +1,20 @@
package io.josemmo.bukkit.plugin.utils;
+import com.palmergames.bukkit.towny.TownyAPI;
+import com.palmergames.bukkit.towny.object.TownyPermission;
+import com.palmergames.bukkit.towny.utils.PlayerCacheUtil;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldguard.LocalPlayer;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.internal.platform.WorldGuardPlatform;
+import com.sk89q.worldguard.protection.flags.Flags;
+import com.sk89q.worldguard.protection.flags.StateFlag;
import io.josemmo.bukkit.plugin.YamipaPlugin;
import me.ryanhamshire.GriefPrevention.GriefPrevention;
import org.bukkit.Bukkit;
import org.bukkit.Location;
+import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -18,6 +24,7 @@
public class Permissions {
@Nullable private static WorldGuard worldGuard = null;
@Nullable private static GriefPrevention griefPrevention = null;
+ @Nullable private static TownyAPI townyApi = null;
static {
try {
@@ -31,44 +38,88 @@ public class Permissions {
} catch (NoClassDefFoundError __) {
// GriefPrevention is not installed
}
+
+ try {
+ townyApi = TownyAPI.getInstance();
+ } catch (NoClassDefFoundError __) {
+ // Towny is not installed
+ }
+ }
+
+ /**
+ * Can build at this block
+ * @param player Player instance
+ * @param location Block location
+ * @return Whether player can build or not
+ */
+ public static boolean canBuild(@NotNull Player player, @NotNull Location location) {
+ return queryWorldGuard(player, location, true)
+ && queryGriefPrevention(player, location, true)
+ && queryTowny(player, location, true);
+ }
+
+ /**
+ * Can destroy this block
+ * @param player Player instance
+ * @param location Block location
+ * @return Whether player can destroy or not
+ */
+ public static boolean canDestroy(@NotNull Player player, @NotNull Location location) {
+ return queryWorldGuard(player, location, false)
+ && queryGriefPrevention(player, location, false)
+ && queryTowny(player, location, false);
+ }
+
+ private static boolean queryWorldGuard(@NotNull Player player, @NotNull Location location, boolean isBuild) {
+ if (worldGuard == null) {
+ return true;
+ }
+ WorldGuardPlatform platform = worldGuard.getPlatform();
+ LocalPlayer wrappedPlayer = WorldGuardPlugin.inst().wrapPlayer(player);
+
+ // Grant if bypass permission is enabled
+ boolean hasBypass = platform.getSessionManager().hasBypass(
+ wrappedPlayer,
+ BukkitAdapter.adapt(location.getWorld())
+ );
+ if (hasBypass) {
+ return true;
+ }
+
+ // Check permission (note "BUILD" flag must always be present)
+ StateFlag flag = isBuild ? Flags.BLOCK_PLACE : Flags.BLOCK_BREAK;
+ return platform.getRegionContainer().createQuery()
+ .testState(BukkitAdapter.adapt(location), wrappedPlayer, Flags.BUILD, flag);
}
- public static boolean canEditBlock(@NotNull Player player, @NotNull Location location) {
- // Check WorldGuard flags
- if (worldGuard != null) {
- WorldGuardPlatform platform = worldGuard.getPlatform();
- LocalPlayer wrappedPlayer = WorldGuardPlugin.inst().wrapPlayer(player);
- boolean canEdit = platform.getRegionContainer().createQuery().testBuild(
- BukkitAdapter.adapt(location),
- wrappedPlayer
- );
- boolean canBypass = platform.getSessionManager().hasBypass(
- wrappedPlayer,
- BukkitAdapter.adapt(location.getWorld())
- );
- if (!canEdit && !canBypass) {
- return false;
- }
+ private static boolean queryGriefPrevention(@NotNull Player player, @NotNull Location location, boolean isBuild) {
+ if (griefPrevention == null) {
+ return true;
}
+ YamipaPlugin plugin = YamipaPlugin.getInstance();
- // Check GriefPrevention permissions
- if (griefPrevention != null) {
- YamipaPlugin plugin = YamipaPlugin.getInstance();
- Callable canEditCallable = () -> griefPrevention.allowBuild(player, location) == null;
- try {
- Boolean canEdit = Bukkit.isPrimaryThread() ?
- canEditCallable.call() :
- Bukkit.getScheduler().callSyncMethod(plugin, canEditCallable).get();
- if (!canEdit) {
- return false;
- }
- } catch (Exception e) {
- plugin.log(Level.SEVERE, "Failed to get player permissions from GriefPrevention", e);
- return false;
- }
+ // Build callable depending on permission to check
+ Callable canEditCallable = isBuild ?
+ () -> griefPrevention.allowBuild(player, location) == null :
+ () -> griefPrevention.allowBreak(player, location.getBlock(), location) == null;
+
+ // Check permission from primary thread
+ try {
+ return Bukkit.isPrimaryThread() ?
+ canEditCallable.call() :
+ Bukkit.getScheduler().callSyncMethod(plugin, canEditCallable).get();
+ } catch (Exception e) {
+ plugin.log(Level.SEVERE, "Failed to get player permissions from GriefPrevention", e);
+ return false;
}
+ }
- // Passed all checks, player can edit this block
- return true;
+ private static boolean queryTowny(@NotNull Player player, @NotNull Location location, boolean isBuild) {
+ if (townyApi == null) {
+ return true;
+ }
+ Material material = location.getBlock().getType();
+ TownyPermission.ActionType type = isBuild ? TownyPermission.ActionType.BUILD : TownyPermission.ActionType.DESTROY;
+ return PlayerCacheUtil.getCachePermission(player, location, material, type);
}
}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index f100005..bb95eb3 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -8,6 +8,7 @@ softdepend:
- Hyperverse
- Multiverse-Core
- My_Worlds
+ - Towny
- WorldGuard
permissions: