" + ChatColor.RESET + " - Download image");
}
+ if (s.hasPermission("yamipa.give")) {
+ s.sendMessage(ChatColor.AQUA + cmd + " give <#> [] []" + ChatColor.RESET + " - Give image items");
+ }
if (s.hasPermission("yamipa.list")) {
s.sendMessage(ChatColor.AQUA + cmd + " list []" + ChatColor.RESET + " - List all images");
}
if (s.hasPermission("yamipa.place")) {
- s.sendMessage(ChatColor.AQUA + cmd + " place []" + ChatColor.RESET + " - Place image");
+ s.sendMessage(ChatColor.AQUA + cmd + " place [] []" + ChatColor.RESET + " - Place image");
}
if (s.hasPermission("yamipa.remove")) {
s.sendMessage(ChatColor.AQUA + cmd + " remove" + ChatColor.RESET + " - Remove a single placed image");
@@ -120,48 +123,58 @@ public static void placeImage(
@NotNull Player player,
@NotNull ImageFile image,
int width,
- int height
+ int height,
+ int flags
) {
- YamipaPlugin plugin = YamipaPlugin.getInstance();
-
// Get image size in blocks
Dimension sizeInPixels = image.getSize();
if (sizeInPixels == null) {
player.sendMessage(ChatColor.RED + "The requested file is not a valid image");
return;
}
- if (height == 0) {
- float imageRatio = (float) sizeInPixels.height / sizeInPixels.width;
- height = Math.round(width * imageRatio);
- height = Math.min(height, FakeImage.MAX_DIMENSION);
- }
- final int finalHeight = height;
+ final int finalHeight = (height == 0) ? FakeImage.getProportionalHeight(sizeInPixels, width) : height;
// Ask player where to place image
SelectBlockTask task = new SelectBlockTask(player);
task.onSuccess((location, face) -> {
- FakeImage existingImage = plugin.getRenderer().getImage(location, face);
- if (existingImage != null) {
- ActionBar.send(player, ChatColor.RED + "There's already an image there!");
- return;
- }
-
- // Create new fake image instance
- Rotation rotation = FakeImage.getRotationFromPlayerEyesight(face, player.getEyeLocation());
- FakeImage fakeImage = new FakeImage(image.getName(), location, face, rotation,
- width, finalHeight, new Date(), player);
-
- // Show loading status to player
- ActionBar loadingActionBar = ActionBar.repeat(player, ChatColor.AQUA + "Loading image...");
- fakeImage.setOnLoadedListener(loadingActionBar::clear);
-
- // Add fake image to renderer
- plugin.getRenderer().addImage(fakeImage);
+ placeImage(player, image, width, finalHeight, flags, location, face);
});
task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image placing canceled"));
task.run("Right click a block to continue");
}
+ public static boolean placeImage(
+ @NotNull Player player,
+ @NotNull ImageFile image,
+ int width,
+ int height,
+ int flags,
+ @NotNull Location location,
+ @NotNull BlockFace face
+ ) {
+ YamipaPlugin plugin = YamipaPlugin.getInstance();
+
+ // Prevent two images occupying the same space
+ FakeImage existingImage = plugin.getRenderer().getImage(location, face);
+ if (existingImage != null) {
+ ActionBar.send(player, ChatColor.RED + "There's already an image there!");
+ return false;
+ }
+
+ // Create new fake image instance
+ Rotation rotation = FakeImage.getRotationFromPlayerEyesight(face, player.getEyeLocation());
+ FakeImage fakeImage = new FakeImage(image.getName(), location, face, rotation,
+ width, height, new Date(), player, flags);
+
+ // Show loading status to player
+ ActionBar loadingActionBar = ActionBar.repeat(player, ChatColor.AQUA + "Loading image...");
+ fakeImage.setOnLoadedListener(loadingActionBar::clear);
+
+ // Add fake image to renderer
+ plugin.getRenderer().addImage(fakeImage);
+ return true;
+ }
+
public static void removeImage(@NotNull Player player) {
ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
@@ -221,19 +234,10 @@ public static void describeImage(@NotNull Player player) {
return;
}
- // Send placed image information to player
- String dateStr = (image.getPlacedAt() == null) ?
- ChatColor.GRAY + "Some point in time" :
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(image.getPlacedAt());
- String playerStr;
- if (image.getPlacedBy().getUniqueId().equals(FakeImage.UNKNOWN_PLAYER_ID)) {
- playerStr = ChatColor.GRAY + "Someone";
- } else if (image.getPlacedBy().getName() == null) {
- playerStr = ChatColor.DARK_AQUA + image.getPlacedBy().getUniqueId().toString();
- } else {
- playerStr = image.getPlacedBy().getName();
- }
+ // Separate previous messages
player.sendMessage("");
+
+ // Basic information
player.sendMessage(ChatColor.GOLD + "Filename: " + ChatColor.RESET + image.getFilename());
player.sendMessage(ChatColor.GOLD + "World: " + ChatColor.RESET +
image.getLocation().getChunk().getWorld().getName());
@@ -245,11 +249,47 @@ public static void describeImage(@NotNull Player player) {
player.sendMessage(ChatColor.GOLD + "Rotation: " + ChatColor.RESET + image.getRotation());
player.sendMessage(ChatColor.GOLD + "Dimensions: " + ChatColor.RESET +
image.getWidth() + "x" + image.getHeight() + " blocks");
+
+ // Speed
int delay = image.getDelay() * 50;
- String delayStr = (delay > 0) ? delay + " ms per step" : ChatColor.GRAY + "Not animatable";
+ String delayStr = (delay > 0) ? delay + " ms per step" : ChatColor.GRAY + "N/A";
player.sendMessage(ChatColor.GOLD + "Speed: " + ChatColor.RESET + delayStr);
+
+ // Placed At
+ String dateStr = (image.getPlacedAt() == null) ?
+ ChatColor.GRAY + "Some point in time" :
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(image.getPlacedAt());
player.sendMessage(ChatColor.GOLD + "Placed At: " + ChatColor.RESET + dateStr);
+
+ // Placed By
+ String playerStr;
+ if (image.getPlacedBy().getUniqueId().equals(FakeImage.UNKNOWN_PLAYER_ID)) {
+ playerStr = ChatColor.GRAY + "Someone";
+ } else if (image.getPlacedBy().getName() == null) {
+ playerStr = ChatColor.DARK_AQUA + image.getPlacedBy().getUniqueId().toString();
+ } else {
+ playerStr = image.getPlacedBy().getName();
+ }
player.sendMessage(ChatColor.GOLD + "Placed By: " + ChatColor.RESET + playerStr);
+
+ // Flags
+ String flagsStr = "";
+ if (image.hasFlag(FakeImage.FLAG_ANIMATABLE)) {
+ flagsStr += ChatColor.AQUA + "ANIM ";
+ }
+ if (image.hasFlag(FakeImage.FLAG_REMOVABLE)) {
+ flagsStr += ChatColor.RED + "REMO ";
+ }
+ if (image.hasFlag(FakeImage.FLAG_DROPPABLE)) {
+ flagsStr += ChatColor.LIGHT_PURPLE + "DROP ";
+ }
+ if (image.hasFlag(FakeImage.FLAG_GLOWING)) {
+ flagsStr += ChatColor.GREEN + "GLOW ";
+ }
+ if (flagsStr.isEmpty()) {
+ flagsStr = ChatColor.GRAY + "N/A";
+ }
+ player.sendMessage(ChatColor.GOLD + "Flags: " + ChatColor.RESET + flagsStr);
});
task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image describing canceled"));
task.run("Right click the image to describe");
@@ -297,4 +337,35 @@ public static void showTopPlayers(@NotNull CommandSender sender) {
++printedLines;
}
}
+
+ public static void giveImageItems(
+ @NotNull CommandSender sender,
+ @NotNull Player player,
+ @NotNull ImageFile image,
+ int amount,
+ int width,
+ int height,
+ int flags
+ ) {
+ // Get image size in blocks
+ Dimension sizeInPixels = image.getSize();
+ if (sizeInPixels == null) {
+ sender.sendMessage(ChatColor.RED + "The requested file is not a valid image");
+ return;
+ }
+ if (height == 0) {
+ height = FakeImage.getProportionalHeight(sizeInPixels, width);
+ }
+
+ // Create item stack
+ ItemStack itemStack = ItemService.getImageItem(image, amount, width, height, flags);
+
+ // Add item stack to player's inventory
+ player.getInventory().addItem(itemStack);
+ sender.sendMessage(
+ ChatColor.ITALIC + "Added " + amount + " " +
+ (amount == 1 ? "image item" : "image items") +
+ " to " + player.getName() + "'s inventory"
+ );
+ }
}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java
index bdc674b..c139bb7 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/commands/ImageCommandBridge.java
@@ -63,6 +63,7 @@ public static void register(@NotNull YamipaPlugin plugin) {
sender.hasPermission("yamipa.clear") ||
sender.hasPermission("yamipa.describe") ||
sender.hasPermission("yamipa.download") ||
+ sender.hasPermission("yamipa.give") ||
sender.hasPermission("yamipa.list") ||
sender.hasPermission("yamipa.place") ||
sender.hasPermission("yamipa.remove") ||
@@ -110,6 +111,41 @@ public static void register(@NotNull YamipaPlugin plugin) {
ImageCommand.downloadImage(sender, (String) args[1], (String) args[2]);
});
+ // Give subcommand
+ root.addSubcommand("give")
+ .withPermission("yamipa.give")
+ .withArgument(new OnlinePlayerArgument("player"))
+ .withArgument(new ImageFileArgument("filename"))
+ .withArgument(new IntegerArgument("amount", 1, 64))
+ .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
+ .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
+ .withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_GIVE_FLAGS))
+ .executes((sender, args) -> {
+ ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
+ (int) args[4], (int) args[5], (int) args[6]);
+ });
+ root.addSubcommand("give")
+ .withPermission("yamipa.give")
+ .withArgument(new OnlinePlayerArgument("player"))
+ .withArgument(new ImageFileArgument("filename"))
+ .withArgument(new IntegerArgument("amount", 1, 64))
+ .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
+ .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
+ .executes((sender, args) -> {
+ ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
+ (int) args[4], (int) args[5], FakeImage.DEFAULT_GIVE_FLAGS);
+ });
+ root.addSubcommand("give")
+ .withPermission("yamipa.give")
+ .withArgument(new OnlinePlayerArgument("player"))
+ .withArgument(new ImageFileArgument("filename"))
+ .withArgument(new IntegerArgument("amount", 1, 64))
+ .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
+ .executes((sender, args) -> {
+ ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
+ (int) args[4], 0, FakeImage.DEFAULT_GIVE_FLAGS);
+ });
+
// List subcommand
root.addSubcommand("list")
.withPermission("yamipa.list")
@@ -125,20 +161,31 @@ public static void register(@NotNull YamipaPlugin plugin) {
});
// Place subcommand
+ root.addSubcommand("place")
+ .withPermission("yamipa.place")
+ .withArgument(new ImageFileArgument("filename"))
+ .withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
+ .withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
+ .withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_PLACE_FLAGS))
+ .executesPlayer((player, args) -> {
+ ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3], (int) args[4]);
+ });
root.addSubcommand("place")
.withPermission("yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
.withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
.executesPlayer((player, args) -> {
- ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3]);
+ ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3],
+ FakeImage.DEFAULT_PLACE_FLAGS);
});
root.addSubcommand("place")
.withPermission("yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
.executesPlayer((player, args) -> {
- ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], 0);
+ ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], 0,
+ FakeImage.DEFAULT_PLACE_FLAGS);
});
// Remove subcommand
diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java
new file mode 100644
index 0000000..cfe4576
--- /dev/null
+++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/ImageFlagsArgument.java
@@ -0,0 +1,99 @@
+package io.josemmo.bukkit.plugin.commands.arguments;
+
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import io.josemmo.bukkit.plugin.renderer.FakeImage;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.CompletableFuture;
+
+public class ImageFlagsArgument extends Argument {
+ private final int defaultFlags;
+
+ /**
+ * Image File Argument constructor
+ * @param name Argument name
+ * @param defaultFlags Default flags
+ */
+ public ImageFlagsArgument(@NotNull String name, int defaultFlags) {
+ super(name);
+ this.defaultFlags = defaultFlags;
+ }
+
+ @Override
+ public @NotNull RequiredArgumentBuilder, ?> build() {
+ return RequiredArgumentBuilder.argument(name, StringArgumentType.greedyString()).suggests(this::getSuggestions);
+ }
+
+ private @NotNull CompletableFuture getSuggestions(
+ @NotNull CommandContext> ctx,
+ @NotNull SuggestionsBuilder builder
+ ) {
+ String input = builder.getRemaining().replaceAll("[^A-Z+\\-,]", "");
+ int lastIndex = Collections.max(
+ Arrays.asList(input.lastIndexOf(","), input.lastIndexOf("+"), input.lastIndexOf("-"))
+ );
+ input = input.substring(0, lastIndex+1);
+
+ // Add suggestions
+ String[] values = new String[] {"ANIM", "REMO", "DROP", "GLOW"};
+ for (String value : values) {
+ builder.suggest(input + value);
+ }
+
+ return builder.buildFuture();
+ }
+
+ @Override
+ public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
+ String value = (String) rawValue;
+ int parsedValue = defaultFlags;
+
+ // Load flags from parts
+ String[] parts = value.split(",");
+ for (String part : parts) {
+ // Parse modifier
+ String modifier = "";
+ if (part.startsWith("+") || part.startsWith("-")) {
+ modifier = part.substring(0, 1);
+ part = part.substring(1);
+ }
+
+ // Parse flag name
+ int flag;
+ switch (part) {
+ case "ANIM":
+ flag = FakeImage.FLAG_ANIMATABLE;
+ break;
+ case "REMO":
+ flag = FakeImage.FLAG_REMOVABLE;
+ break;
+ case "DROP":
+ flag = FakeImage.FLAG_DROPPABLE;
+ break;
+ case "GLOW":
+ flag = FakeImage.FLAG_GLOWING;
+ break;
+ default:
+ throw newException("Unrecognized flag \"" + part + "\"");
+ }
+
+ // Apply flag to value
+ if (modifier.equals("+")) {
+ parsedValue |= flag;
+ } else if (modifier.equals("-")) {
+ parsedValue &= ~flag;
+ } else {
+ parsedValue = flag;
+ }
+ }
+
+ return parsedValue;
+ }
+}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java
new file mode 100644
index 0000000..51350f2
--- /dev/null
+++ b/src/main/java/io/josemmo/bukkit/plugin/commands/arguments/OnlinePlayerArgument.java
@@ -0,0 +1,55 @@
+package io.josemmo.bukkit.plugin.commands.arguments;
+
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public class OnlinePlayerArgument extends StringArgument {
+ /**
+ * Online player argument constructor
+ * @param name Argument name
+ */
+ public OnlinePlayerArgument(@NotNull String name) {
+ super(name);
+ }
+
+ @Override
+ public @NotNull RequiredArgumentBuilder, ?> build() {
+ return super.build().suggests(this::getSuggestions);
+ }
+
+ @Override
+ public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
+ Player player = getAllowedValues().get((String) rawValue);
+ if (player == null) {
+ throw newException("Expected online player (name or UUID)");
+ }
+ return player;
+ }
+
+ private @NotNull CompletableFuture getSuggestions(
+ @NotNull CommandContext> ctx,
+ @NotNull SuggestionsBuilder builder
+ ) {
+ getAllowedValues().keySet().forEach(builder::suggest);
+ return builder.buildFuture();
+ }
+
+ private @NotNull Map getAllowedValues() {
+ Map values = new HashMap<>();
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ values.put(player.getUniqueId().toString(), player);
+ values.put(player.getName(), player);
+ }
+ return values;
+ }
+}
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 91510d1..9750bf2 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeImage.java
@@ -10,6 +10,7 @@
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.awt.*;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -21,6 +22,16 @@ public class FakeImage extends FakeEntity {
public static final int MIN_DELAY = 1; // Minimum step delay in 50ms intervals (50ms / 50ms)
public static final int MAX_DELAY = 50; // Maximum step delay in 50ms intervals (5000ms / 50ms)
public static final UUID UNKNOWN_PLAYER_ID = new UUID(0, 0);
+
+ // Flags
+ public static final int FLAG_ANIMATABLE = 1; // Whether image is allowed to animate multiple steps
+ public static final int FLAG_REMOVABLE = 2; // Whether image can be removed by a player using the interact button
+ public static final int FLAG_DROPPABLE = 4; // Whether image will drop an image item when removed by a player
+ public static final int FLAG_GLOWING = 8; // Whether image glows in the dark
+ public static final int DEFAULT_PLACE_FLAGS = FLAG_ANIMATABLE;
+ public static final int DEFAULT_GIVE_FLAGS = FLAG_ANIMATABLE | FLAG_REMOVABLE | FLAG_DROPPABLE;
+
+ // Instance properties
private static boolean animateImages = false;
private final String filename;
private final Location location;
@@ -30,6 +41,7 @@ public class FakeImage extends FakeEntity {
private final int height;
private final Date placedAt;
private final OfflinePlayer placedBy;
+ private final int flags;
private final BiFunction getLocationVector;
private Runnable onLoadedListener = null;
@@ -44,7 +56,7 @@ public class FakeImage extends FakeEntity {
private int currentStep = -1; // Current animation step
/**
- * Enable image animation
+ * Enable plugin-wide image animation support
*/
public static void enableAnimation() {
animateImages = true;
@@ -84,16 +96,30 @@ public static boolean isAnimationEnabled() {
}
}
+ /**
+ * Get proportional height
+ * @param sizeInPixels Image file dimension in pixels
+ * @param width Desired width in blocks
+ * @return Height in blocks (capped at FakeImage.MAX_DIMENSION
)
+ */
+ public static int getProportionalHeight(@NotNull Dimension sizeInPixels, int width) {
+ float imageRatio = (float) sizeInPixels.height / sizeInPixels.width;
+ int height = Math.round(width * imageRatio);
+ height = Math.min(height, MAX_DIMENSION);
+ return height;
+ }
+
/**
* Class constructor
- * @param filename Image filename
- * @param location Top-left corner where image will be placed
- * @param face Block face
- * @param rotation Image rotation
- * @param width Width in blocks
- * @param height Height in blocks
- * @param placedAt Placed at
- * @param placedBy Placed by
+ * @param filename Image filename
+ * @param location Top-left corner where image will be placed
+ * @param face Block face
+ * @param rotation Image rotation
+ * @param width Width in blocks
+ * @param height Height in blocks
+ * @param placedAt Placed at
+ * @param placedBy Placed by
+ * @param flags Flags
*/
public FakeImage(
@NotNull String filename,
@@ -103,7 +129,8 @@ public FakeImage(
int width,
int height,
@Nullable Date placedAt,
- @NotNull OfflinePlayer placedBy
+ @NotNull OfflinePlayer placedBy,
+ int flags
) {
this.filename = filename;
this.location = location;
@@ -113,6 +140,7 @@ public FakeImage(
this.height = height;
this.placedAt = placedAt;
this.placedBy = placedBy;
+ this.flags = flags;
// Define function for retrieving item frame positional vector from pair
if (face == BlockFace.SOUTH) {
@@ -220,6 +248,23 @@ public int getHeight() {
return placedBy;
}
+ /**
+ * Get flags
+ * @return Flags
+ */
+ public int getFlags() {
+ return flags;
+ }
+
+ /**
+ * Has flag
+ * @param flag Flag to check
+ * @return Whether instance has given flag or not
+ */
+ public boolean hasFlag(int flag) {
+ return ((flags & flag) == flag);
+ }
+
/**
* Get image delay
* @return Image delay in 50ms intervals
@@ -304,10 +349,11 @@ private void load() {
// Generate frames
frames = new FakeItemFrame[width*height];
+ boolean glowing = hasFlag(FLAG_GLOWING);
for (int col=0; col 1) {
+ if (animateImages && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) {
animatingPlayers.add(player);
if (task == null) {
task = plugin.getScheduler().scheduleAtFixedRate(
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 611d5d4..dc92415 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/FakeItemFrame.java
@@ -6,6 +6,7 @@
import io.josemmo.bukkit.plugin.packets.DestroyEntityPacket;
import io.josemmo.bukkit.plugin.packets.EntityMetadataPacket;
import io.josemmo.bukkit.plugin.packets.SpawnEntityPacket;
+import io.josemmo.bukkit.plugin.utils.Internals;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Rotation;
@@ -19,11 +20,13 @@
public class FakeItemFrame extends FakeEntity {
public static final int MIN_FRAME_ID = Integer.MAX_VALUE / 4;
public static final int MAX_FRAME_ID = Integer.MAX_VALUE;
+ private static final boolean SUPPORTS_GLOWING = Internals.MINECRAFT_VERSION >= 17;
private static final AtomicInteger lastFrameId = new AtomicInteger(MAX_FRAME_ID);
private final int id;
private final Location location;
private final BlockFace face;
private final Rotation rotation;
+ private final boolean glowing;
private final FakeMap[] maps;
/**
@@ -44,13 +47,21 @@ private static int getNextId() {
* @param location Frame location
* @param face Block face
* @param rotation Frame rotation
+ * @param glowing Whether is glowing or regular frame
* @param maps Fake maps to animate
*/
- public FakeItemFrame(@NotNull Location location, @NotNull BlockFace face, @NotNull Rotation rotation, @NotNull FakeMap[] maps) {
+ public FakeItemFrame(
+ @NotNull Location location,
+ @NotNull BlockFace face,
+ @NotNull Rotation rotation,
+ boolean glowing,
+ @NotNull FakeMap[] maps
+ ) {
this.id = getNextId();
this.location = location;
this.face = face;
this.rotation = rotation;
+ this.glowing = glowing;
this.maps = maps;
plugin.fine("Created FakeItemFrame#" + this.id + " using " + this.maps.length + " FakeMap(s)");
}
@@ -100,11 +111,12 @@ public void spawn(@NotNull Player player) {
// Create item frame entity
SpawnEntityPacket framePacket = new SpawnEntityPacket();
framePacket.setId(id)
- .setEntityType(EntityType.ITEM_FRAME)
+ .setEntityType((glowing && SUPPORTS_GLOWING) ? EntityType.GLOW_ITEM_FRAME : EntityType.ITEM_FRAME)
.setPosition(x, y, z)
.setRotation(pitch, yaw)
.setData(orientation);
tryToSendPacket(player, framePacket);
+ plugin.fine("Spawned FakeItemFrame#" + this.id + " for Player#" + player.getName());
// Send pixels for all linked maps
for (FakeMap map : maps) {
@@ -144,5 +156,6 @@ public void destroy(@NotNull Player player) {
DestroyEntityPacket destroyPacket = new DestroyEntityPacket();
destroyPacket.setId(id);
tryToSendPacket(player, destroyPacket);
+ plugin.fine("Destroyed FakeItemFrame#" + this.id + " for Player#" + player.getName());
}
}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java
index d7c6764..7e8329f 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ImageRenderer.java
@@ -106,7 +106,12 @@ private void loadConfig() {
UUID.fromString(row[10]) :
FakeImage.UNKNOWN_PLAYER_ID;
OfflinePlayer placedBy = Bukkit.getOfflinePlayer(placedById);
- addImage(new FakeImage(filename, location, face, rotation, width, height, placedAt, placedBy), true);
+ int flags = (row.length > 11) ?
+ Math.max(Integer.parseInt(row[11]), 0) :
+ FakeImage.DEFAULT_PLACE_FLAGS;
+ FakeImage fakeImage = new FakeImage(filename, location, face, rotation, width, height,
+ placedAt, placedBy, flags);
+ addImage(fakeImage, true);
} catch (Exception e) {
plugin.log(Level.SEVERE, "Invalid fake image properties: " + String.join(";", row), e);
}
@@ -144,7 +149,8 @@ private void saveConfig() {
fakeImage.getWidth() + "",
fakeImage.getHeight() + "",
(fakeImage.getPlacedAt() == null) ? "" : (fakeImage.getPlacedAt().getTime() / 1000) + "",
- placedById.equals(FakeImage.UNKNOWN_PLAYER_ID) ? "" : placedById.toString()
+ placedById.equals(FakeImage.UNKNOWN_PLAYER_ID) ? "" : placedById.toString(),
+ fakeImage.getFlags() + ""
};
config.addRow(row);
}
@@ -164,7 +170,10 @@ private void saveConfig() {
* @param isInit TRUE if called during renderer startup, FALSE otherwise
*/
public void addImage(@NotNull FakeImage image, boolean isInit) {
- for (WorldAreaId worldAreaId : image.getWorldAreaIds()) {
+ WorldAreaId[] imageWorldAreaIds = image.getWorldAreaIds();
+
+ // Add image to world area(s)
+ for (WorldAreaId worldAreaId : imageWorldAreaIds) {
WorldArea worldArea = worldAreas.computeIfAbsent(worldAreaId, __ -> {
plugin.fine("Created WorldArea#(" + worldAreaId + ")");
return new WorldArea(worldAreaId);
@@ -180,6 +189,11 @@ public void addImage(@NotNull FakeImage image, boolean isInit) {
// Increment count of placed images by player
UUID placedById = image.getPlacedBy().getUniqueId();
imagesCountByPlayer.compute(placedById, (__, prev) -> (prev == null) ? 1 : prev+1);
+
+ // Spawn image in players nearby
+ for (Player player : getPlayersInNeighborhood(imageWorldAreaIds)) {
+ image.spawn(player);
+ }
}
/**
@@ -242,7 +256,16 @@ public void addImage(@NotNull FakeImage image) {
* @param image Fake image instance
*/
public void removeImage(@NotNull FakeImage image) {
- for (WorldAreaId worldAreaId : image.getWorldAreaIds()) {
+ WorldAreaId[] imageWorldAreaIds = image.getWorldAreaIds();
+
+ // Destroy image from players nearby
+ for (Player player : getPlayersInNeighborhood(imageWorldAreaIds)) {
+ image.destroy(player);
+ }
+ image.invalidate();
+
+ // Remove image from world area(s)
+ for (WorldAreaId worldAreaId : imageWorldAreaIds) {
WorldArea worldArea = worldAreas.get(worldAreaId);
worldArea.removeImage(image);
if (!worldArea.hasImages()) {
@@ -250,7 +273,6 @@ public void removeImage(@NotNull FakeImage image) {
worldAreas.remove(worldAreaId);
}
}
- image.invalidate();
// Set configuration changed flag
hasConfigChanged.set(true);
@@ -308,6 +330,28 @@ public int size() {
return instances;
}
+ /**
+ * Get players in neighborhood
+ * @param ids World area IDs to compute neighborhood from
+ * @return Players inside those world areas
+ */
+ private @NotNull Set getPlayersInNeighborhood(@NotNull WorldAreaId[] ids) {
+ Set neighborhood = new HashSet<>();
+ for (WorldAreaId worldAreaId : ids) {
+ Collections.addAll(neighborhood, worldAreaId.getNeighborhood());
+ }
+
+ Set players = new HashSet<>();
+ for (Map.Entry entry : playersLocation.entrySet()) {
+ if (neighborhood.contains(entry.getValue())) {
+ Player player = Bukkit.getPlayer(entry.getKey());
+ players.add(player);
+ }
+ }
+
+ return players;
+ }
+
/**
* On player location change
* @param player Player instance
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java
new file mode 100644
index 0000000..5cc706b
--- /dev/null
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/ItemService.java
@@ -0,0 +1,174 @@
+package io.josemmo.bukkit.plugin.renderer;
+
+import io.josemmo.bukkit.plugin.YamipaPlugin;
+import io.josemmo.bukkit.plugin.commands.ImageCommand;
+import io.josemmo.bukkit.plugin.storage.ImageFile;
+import io.josemmo.bukkit.plugin.utils.InteractWithEntityListener;
+import org.bukkit.*;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.event.hanging.HangingPlaceEvent;
+import org.bukkit.event.inventory.PrepareItemCraftEvent;
+import org.bukkit.inventory.CraftingInventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.PlayerInventory;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import java.util.Collections;
+import java.util.Objects;
+
+public class ItemService extends InteractWithEntityListener implements Listener {
+ private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
+ private static final NamespacedKey NSK_FILENAME = new NamespacedKey(plugin, "filename");
+ private static final NamespacedKey NSK_WIDTH = new NamespacedKey(plugin, "width");
+ private static final NamespacedKey NSK_HEIGHT = new NamespacedKey(plugin, "height");
+ private static final NamespacedKey NSK_FLAGS = new NamespacedKey(plugin, "flags");
+
+ /**
+ * Get image item
+ * @param image Image file
+ * @param amount Stack amount
+ * @param width Image width in blocks
+ * @param height Image height in blocks
+ * @param flags Image flags
+ * @return Image item
+ */
+ public static @NotNull ItemStack getImageItem(@NotNull ImageFile image, int amount, int width, int height, int flags) {
+ ItemStack itemStack = new ItemStack(Material.ITEM_FRAME, amount);
+ ItemMeta itemMeta = Objects.requireNonNull(itemStack.getItemMeta());
+
+ // Set metadata
+ PersistentDataContainer itemData = itemMeta.getPersistentDataContainer();
+ itemMeta.setDisplayName(image.getName() + ChatColor.AQUA + " (" + width + "x" + height + ")");
+ itemMeta.setLore(Collections.singletonList("Yamipa image"));
+ itemData.set(NSK_FILENAME, PersistentDataType.STRING, image.getName());
+ itemData.set(NSK_WIDTH, PersistentDataType.INTEGER, width);
+ itemData.set(NSK_HEIGHT, PersistentDataType.INTEGER, height);
+ itemData.set(NSK_FLAGS, PersistentDataType.INTEGER, flags);
+ itemStack.setItemMeta(itemMeta);
+
+ return itemStack;
+ }
+
+ /**
+ * Start service
+ */
+ public void start() {
+ register();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ /**
+ * Stop service
+ */
+ public void stop() {
+ unregister();
+ HandlerList.unregisterAll(this);
+ }
+
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
+ CraftingInventory inventory = event.getInventory();
+ for (@Nullable ItemStack item : inventory.getMatrix()) {
+ if (item == null) continue;
+
+ // Get metadata from item
+ ItemMeta itemMeta = item.getItemMeta();
+ if (itemMeta == null) continue;
+
+ // Prevent crafting recipes with image items
+ if (itemMeta.getPersistentDataContainer().has(NSK_FILENAME, PersistentDataType.STRING)) {
+ inventory.setResult(new ItemStack(Material.AIR));
+ break;
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onPlaceItem(@NotNull HangingPlaceEvent event) {
+ Player player = event.getPlayer();
+ ItemStack item = event.getItemStack();
+ if (player == null || item == null) return;
+
+ // Get metadata from item
+ ItemMeta itemMeta = item.getItemMeta();
+ if (itemMeta == null) return;
+ PersistentDataContainer itemData = itemMeta.getPersistentDataContainer();
+ String filename = itemData.get(NSK_FILENAME, PersistentDataType.STRING);
+ if (filename == null) return;
+ Integer width = itemData.get(NSK_WIDTH, PersistentDataType.INTEGER);
+ Integer height = itemData.get(NSK_HEIGHT, PersistentDataType.INTEGER);
+ Integer flags = itemData.get(NSK_FLAGS, PersistentDataType.INTEGER);
+ if (width == null || height == null || flags == null) {
+ plugin.warning(player + " tried to place corrupted image item (missing width/height/flags properties)");
+ return;
+ }
+
+ // Validate filename
+ ImageFile image = YamipaPlugin.getInstance().getStorage().get(filename);
+ if (image == null) {
+ plugin.warning(player + " tried to place corrupted image item (\"" + filename + "\" no longer exists)");
+ player.sendMessage(ChatColor.RED + "Image file \"" + filename + "\" no longer exists");
+ return;
+ }
+
+ // Prevent item frame placing
+ event.setCancelled(true);
+
+ // Try to place image in world
+ Location location = event.getBlock().getLocation();
+ boolean success = ImageCommand.placeImage(player, image, width, height, flags, location, event.getBlockFace());
+ if (!success) return;
+
+ // Decrement item from player's inventory
+ if (player.getGameMode() == GameMode.CREATIVE) return;
+ PlayerInventory inventory = player.getInventory();
+ int itemIndex = inventory.first(item);
+ int amount = item.getAmount();
+ if (amount > 1) {
+ item.setAmount(amount - 1);
+ inventory.setItem(itemIndex, item);
+ } else {
+ inventory.setItem(itemIndex, new ItemStack(Material.AIR));
+ }
+ }
+
+ @Override
+ public boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
+ ImageRenderer renderer = plugin.getRenderer();
+ Location location = block.getLocation();
+
+ // Has the player clicked a removable placed image?
+ FakeImage image = renderer.getImage(location, face);
+ if (image == null || !image.hasFlag(FakeImage.FLAG_REMOVABLE)) return true;
+
+ // Remove image from renderer
+ renderer.removeImage(image);
+
+ // Drop image item
+ if (player.getGameMode() == GameMode.SURVIVAL && image.hasFlag(FakeImage.FLAG_DROPPABLE)) {
+ ImageFile imageFile = Objects.requireNonNull(image.getFile());
+ ItemStack imageItem = getImageItem(imageFile, 1, image.getWidth(), image.getHeight(), image.getFlags());
+ Location dropLocation = location.clone().add(0.5, -0.5, 0.5).add(face.getDirection());
+ Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
+ block.getWorld().dropItem(dropLocation, imageItem);
+ });
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onInteract(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
+ // Intentionally left blank
+ return true;
+ }
+}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldArea.java b/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldArea.java
index fcaf561..d68288f 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldArea.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/renderer/WorldArea.java
@@ -61,9 +61,6 @@ public boolean hasImages() {
*/
public void addImage(@NotNull FakeImage image) {
fakeImages.add(image);
- for (Player player : players) {
- image.spawn(player);
- }
}
/**
@@ -72,9 +69,6 @@ public void addImage(@NotNull FakeImage image) {
*/
public void removeImage(@NotNull FakeImage image) {
fakeImages.remove(image);
- for (Player player : players) {
- image.destroy(player);
- }
}
/**
diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java b/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java
new file mode 100644
index 0000000..8a288cc
--- /dev/null
+++ b/src/main/java/io/josemmo/bukkit/plugin/utils/InteractWithEntityListener.java
@@ -0,0 +1,120 @@
+package io.josemmo.bukkit.plugin.utils;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.ListenerPriority;
+import com.comphenix.protocol.events.ListeningWhitelist;
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.events.PacketListener;
+import com.comphenix.protocol.wrappers.EnumWrappers;
+import io.josemmo.bukkit.plugin.YamipaPlugin;
+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 java.util.List;
+
+public abstract class InteractWithEntityListener implements PacketListener {
+ public static final int MAX_BLOCK_DISTANCE = 5; // Server should only accept entities within a 4-block radius
+
+ /**
+ * On player attack listener
+ * @param player Initiating player
+ * @param block Target block
+ * @param face Target block face
+ * @return Whether to allow original event (true
) or not (false
)
+ */
+ public abstract boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face);
+
+ /**
+ * On player interact listener
+ * @param player Initiating player
+ * @param block Target block
+ * @param face Target block face
+ * @return Whether to allow original event (true
) or not (false
)
+ */
+ public abstract boolean onInteract(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face);
+
+ /**
+ * Get listener priority
+ * @return Listener priority
+ */
+ public @NotNull ListenerPriority getPriority() {
+ return ListenerPriority.LOWEST;
+ }
+
+ /**
+ * Register listener
+ */
+ public void register() {
+ ProtocolLibrary.getProtocolManager().addPacketListener(this);
+ }
+
+ /**
+ * Unregister listener
+ */
+ public void unregister() {
+ ProtocolLibrary.getProtocolManager().removePacketListener(this);
+ }
+
+ @Override
+ public final void onPacketReceiving(@NotNull PacketEvent event) {
+ Player player = event.getPlayer();
+
+ // Discard out-of-range packets
+ List lastTwoTargetBlocks = player.getLastTwoTargetBlocks(null, MAX_BLOCK_DISTANCE);
+ if (lastTwoTargetBlocks.size() != 2) return;
+ Block targetBlock = lastTwoTargetBlocks.get(1);
+ if (!targetBlock.getType().isOccluding()) return;
+
+ // Get target block face
+ Block adjacentBlock = lastTwoTargetBlocks.get(0);
+ BlockFace targetBlockFace = targetBlock.getFace(adjacentBlock);
+ if (targetBlockFace == null) return;
+
+ // Get action
+ EnumWrappers.EntityUseAction action;
+ if (Internals.MINECRAFT_VERSION < 17) {
+ action = event.getPacket().getEntityUseActions().read(0);
+ } else {
+ action = event.getPacket().getEnumEntityUseActions().read(0).getAction();
+ }
+
+ // Notify handler
+ boolean allowEvent = true;
+ if (action == EnumWrappers.EntityUseAction.ATTACK) {
+ allowEvent = onAttack(player, targetBlock, targetBlockFace);
+ } else if (action == EnumWrappers.EntityUseAction.INTERACT_AT) {
+ allowEvent = onInteract(player, targetBlock, targetBlockFace);
+ }
+
+ // Cancel event (if needed)
+ if (!allowEvent) {
+ event.setCancelled(true);
+ }
+ }
+
+ @Override
+ public final void onPacketSending(PacketEvent event) {
+ // Intentionally left blank
+ }
+
+ @Override
+ public final ListeningWhitelist getReceivingWhitelist() {
+ return ListeningWhitelist.newBuilder()
+ .priority(getPriority())
+ .types(PacketType.Play.Client.USE_ENTITY)
+ .build();
+ }
+
+ @Override
+ public final ListeningWhitelist getSendingWhitelist() {
+ return ListeningWhitelist.EMPTY_WHITELIST;
+ }
+
+ @Override
+ public final Plugin getPlugin() {
+ return YamipaPlugin.getInstance();
+ }
+}
diff --git a/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java b/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java
index 53cfba1..e5ff860 100644
--- a/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java
+++ b/src/main/java/io/josemmo/bukkit/plugin/utils/SelectBlockTask.java
@@ -1,11 +1,6 @@
package io.josemmo.bukkit.plugin.utils;
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.ListeningWhitelist;
-import com.comphenix.protocol.events.PacketEvent;
-import com.comphenix.protocol.events.PacketListener;
-import com.comphenix.protocol.wrappers.EnumWrappers;
+import com.comphenix.protocol.events.ListenerPriority;
import io.josemmo.bukkit.plugin.YamipaPlugin;
import org.bukkit.ChatColor;
import org.bukkit.Location;
@@ -17,13 +12,13 @@
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
+import org.bukkit.event.player.PlayerAnimationEvent;
+import org.bukkit.event.player.PlayerAnimationType;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
-import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiConsumer;
@@ -31,7 +26,7 @@
public class SelectBlockTask {
private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
private static final Map instances = new HashMap<>();
- private static PlayerInteractionListener listener = null;
+ private static SelectBlockTaskListener listener = null;
private final Player player;
private BiConsumer success;
private Runnable failure;
@@ -76,9 +71,8 @@ public void run(@NotNull String helpMessage) {
// Create listener singleton if needed
if (listener == null) {
- listener = new PlayerInteractionListener();
- plugin.getServer().getPluginManager().registerEvents(listener, plugin);
- ProtocolLibrary.getProtocolManager().addPacketListener(listener);
+ listener = new SelectBlockTaskListener();
+ listener.register();
plugin.fine("Created PlayerInteractionListener singleton");
}
@@ -100,49 +94,66 @@ public void cancel() {
// Destroy listener singleton if no more active tasks
if (instances.isEmpty()) {
- HandlerList.unregisterAll(listener);
- ProtocolLibrary.getProtocolManager().removePacketListener(listener);
+ listener.unregister();
listener = null;
- plugin.fine("Destroyed PlayerInteractionListener singleton");
+ plugin.fine("Destroyed SelectBlockTaskListener singleton");
}
}
/**
* Internal listener for handling player events
*/
- private static class PlayerInteractionListener implements Listener, PacketListener {
- @EventHandler
- public void onPlayerInteraction(PlayerInteractEvent event) {
+ private static class SelectBlockTaskListener extends InteractWithEntityListener implements Listener {
+ @Override
+ public void register() {
+ super.register();
+ plugin.getServer().getPluginManager().registerEvents(this, plugin);
+ }
+
+ @Override
+ public void unregister() {
+ super.unregister();
+ HandlerList.unregisterAll(this);
+ }
+
+ @Override
+ public @NotNull ListenerPriority getPriority() {
+ return ListenerPriority.LOW;
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
+ public void onBlockInteraction(@NotNull PlayerInteractEvent event) {
Action action = event.getAction();
+ Player player = event.getPlayer();
Block block = event.getClickedBlock();
+ if (block == null) return;
+ BlockFace face = event.getBlockFace();
- // Get task responsible for handling this event
- UUID uuid = event.getPlayer().getUniqueId();
- SelectBlockTask task = instances.get(uuid);
- if (task == null) return;
-
- // Player canceled the task
+ // Handle failure event
if (action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK) {
event.setCancelled(true);
- task.cancel();
- if (task.failure != null) {
- task.failure.run();
- }
+ handle(player, null, null);
return;
}
- // Player selected a block
- if (action == Action.RIGHT_CLICK_BLOCK && block != null) {
+ // Handle success event
+ if (action == Action.RIGHT_CLICK_BLOCK) {
event.setCancelled(true);
- task.cancel();
- if (task.success != null) {
- task.success.accept(block.getLocation(), event.getBlockFace());
- }
+ handle(player, block, face);
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onArmSwing(@NotNull PlayerAnimationEvent event) {
+ if (event.getAnimationType() != PlayerAnimationType.ARM_SWING) {
+ // Sanity check, vanilla Minecraft does not have any other player animation type
+ return;
}
+ handle(event.getPlayer(), null, null);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
- public void onPlayerQuit(PlayerQuitEvent event) {
+ public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
SelectBlockTask task = instances.get(uuid);
if (task != null) {
@@ -151,68 +162,38 @@ public void onPlayerQuit(PlayerQuitEvent event) {
}
@Override
- public void onPacketReceiving(PacketEvent event) {
- Player player = event.getPlayer();
+ public boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
+ handle(player, null, null);
+ return false;
+ }
+
+ @Override
+ public boolean onInteract(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
+ handle(player, block, face);
+ return false;
+ }
+ private void handle(@NotNull Player player, @Nullable Block block, @Nullable BlockFace face) {
// Get task responsible for handling this event
UUID uuid = player.getUniqueId();
SelectBlockTask task = instances.get(uuid);
if (task == null) return;
- // Get action
- EnumWrappers.EntityUseAction action;
- if (Internals.MINECRAFT_VERSION < 17) {
- action = event.getPacket().getEntityUseActions().read(0);
- } else {
- action = event.getPacket().getEnumEntityUseActions().read(0).getAction();
- }
+ // Cancel task
+ task.cancel();
- // Player left clicked an entity
- if (action == EnumWrappers.EntityUseAction.ATTACK) {
- event.setCancelled(true);
- task.cancel();
+ // Notify failure listener
+ if (block == null || face == null) {
if (task.failure != null) {
task.failure.run();
}
return;
}
- // Player right clicked an entity
- if (action == EnumWrappers.EntityUseAction.INTERACT_AT) {
- int maxDistance = 5; // Server should only accept entities within a 4-block radius
- List lastTwoTargetBlocks = player.getLastTwoTargetBlocks(null, maxDistance);
- if (lastTwoTargetBlocks.size() != 2) return;
- Block targetBlock = lastTwoTargetBlocks.get(1);
- Block adjacentBlock = lastTwoTargetBlocks.get(0);
- if (!targetBlock.getType().isOccluding()) return;
-
- BlockFace targetBlockFace = targetBlock.getFace(adjacentBlock);
- event.setCancelled(true);
- task.cancel();
- if (task.success != null) {
- task.success.accept(targetBlock.getLocation(), targetBlockFace);
- }
+ // Notify success listener
+ if (task.success != null) {
+ task.success.accept(block.getLocation(), face);
}
}
-
- @Override
- public void onPacketSending(PacketEvent event) {
- // Intentionally left blank
- }
-
- @Override
- public ListeningWhitelist getReceivingWhitelist() {
- return ListeningWhitelist.newBuilder().types(PacketType.Play.Client.USE_ENTITY).build();
- }
-
- @Override
- public ListeningWhitelist getSendingWhitelist() {
- return ListeningWhitelist.EMPTY_WHITELIST;
- }
-
- @Override
- public Plugin getPlugin() {
- return plugin;
- }
}
}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index b2d7c35..475c224 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -3,7 +3,7 @@ version: ${project.version}
main: io.josemmo.bukkit.plugin.YamipaPlugin
api-version: 1.13
depend: [ProtocolLib]
-softdepend: [Multiverse-Core]
+softdepend: [Multiverse-Core, Hyperverse]
permissions:
yamipa.*:
@@ -12,6 +12,7 @@ permissions:
yamipa.clear: true
yamipa.describe: true
yamipa.download: true
+ yamipa.give: true
yamipa.list: true
yamipa.place: true
yamipa.remove: true
@@ -22,6 +23,8 @@ permissions:
default: op
yamipa.download:
default: op
+ yamipa.give:
+ default: op
yamipa.list:
default: op
yamipa.place: