500 lines
19 KiB
Java
500 lines
19 KiB
Java
package de.craftlancer.imagemaps;
|
|
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.logging.Level;
|
|
import java.util.stream.Collectors;
|
|
|
|
import javax.imageio.ImageIO;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.Rotation;
|
|
import org.bukkit.block.Block;
|
|
import org.bukkit.block.BlockFace;
|
|
import org.bukkit.configuration.Configuration;
|
|
import org.bukkit.configuration.ConfigurationSection;
|
|
import org.bukkit.configuration.file.FileConfiguration;
|
|
import org.bukkit.configuration.file.YamlConfiguration;
|
|
import org.bukkit.configuration.serialization.ConfigurationSerialization;
|
|
import org.bukkit.entity.EntityType;
|
|
import org.bukkit.entity.Hanging;
|
|
import org.bukkit.entity.ItemFrame;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.Listener;
|
|
import org.bukkit.event.block.Action;
|
|
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
|
import org.bukkit.event.player.PlayerInteractEvent;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.inventory.meta.MapMeta;
|
|
import org.bukkit.map.MapView;
|
|
import org.bukkit.plugin.java.JavaPlugin;
|
|
import org.bukkit.scheduler.BukkitRunnable;
|
|
|
|
import de.craftlancer.core.LambdaRunnable;
|
|
import de.craftlancer.core.SemanticVersion;
|
|
import de.craftlancer.core.Utils;
|
|
import de.craftlancer.core.util.MessageLevel;
|
|
import de.craftlancer.core.util.MessageUtil;
|
|
import de.craftlancer.core.util.Tuple;
|
|
import net.md_5.bungee.api.ChatColor;
|
|
import net.md_5.bungee.api.chat.BaseComponent;
|
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
|
import net.md_5.bungee.api.chat.TextComponent;
|
|
|
|
public class ImageMaps extends JavaPlugin implements Listener {
|
|
private static final String CONFIG_VERSION_KEY = "storageVersion";
|
|
private static final int CONFIG_VERSION = 1;
|
|
private static final long AUTOSAVE_PERIOD = 18000L; // 15 minutes
|
|
|
|
public static final String PLACEMENT_METADATA = "imagemaps.place";
|
|
|
|
public static final int MAP_WIDTH = 128;
|
|
public static final int MAP_HEIGHT = 128;
|
|
private static final String IMAGES_DIR = "images";
|
|
|
|
private Map<String, BufferedImage> imageCache = new HashMap<>();
|
|
private Map<ImageMap, Integer> maps = new HashMap<>();
|
|
|
|
static {
|
|
ConfigurationSerialization.registerClass(ImageMap.class);
|
|
}
|
|
|
|
@Override
|
|
public void onEnable() {
|
|
BaseComponent prefix = new TextComponent(
|
|
new ComponentBuilder("[").color(ChatColor.GRAY).append("ImageMaps").color(ChatColor.AQUA).append("]").color(ChatColor.GRAY).create());
|
|
MessageUtil.registerPlugin(this, prefix, ChatColor.GRAY, ChatColor.YELLOW, ChatColor.RED, ChatColor.DARK_RED, ChatColor.DARK_AQUA);
|
|
|
|
if (!new File(getDataFolder(), IMAGES_DIR).exists())
|
|
new File(getDataFolder(), IMAGES_DIR).mkdirs();
|
|
|
|
getCommand("imagemap").setExecutor(new ImageMapCommandHandler(this));
|
|
getServer().getPluginManager().registerEvents(this, this);
|
|
|
|
loadMaps();
|
|
|
|
new LambdaRunnable(this::saveMaps).runTaskTimer(this, AUTOSAVE_PERIOD, AUTOSAVE_PERIOD);
|
|
}
|
|
|
|
@Override
|
|
public void onDisable() {
|
|
saveMaps();
|
|
}
|
|
|
|
@EventHandler(ignoreCancelled = true)
|
|
public void onToggleFrameProperty(PlayerInteractEntityEvent event) {
|
|
if (!isInvisibilitySupported())
|
|
return;
|
|
|
|
if (event.getRightClicked().getType() != EntityType.ITEM_FRAME)
|
|
return;
|
|
|
|
ItemFrame frame = (ItemFrame) event.getRightClicked();
|
|
Player p = event.getPlayer();
|
|
|
|
if (p.getInventory().getItemInMainHand().getType() != Material.WOODEN_HOE)
|
|
return;
|
|
|
|
if (p.isSneaking() && p.hasPermission("imagemaps.toggleFixed")) {
|
|
frame.setFixed(!frame.isFixed());
|
|
MessageUtil.sendMessage(this, p, MessageLevel.INFO, String.format("Frame set to %s.", frame.isFixed() ? "fixed" : "unfixed"));
|
|
}
|
|
else if (p.hasPermission("imagemaps.toggleVisible")) {
|
|
frame.setVisible(!frame.isVisible());
|
|
MessageUtil.sendMessage(this, p, MessageLevel.INFO, String.format("Frame set to %s.", frame.isVisible() ? "visible" : "invisible"));
|
|
}
|
|
|
|
event.setCancelled(true);
|
|
}
|
|
|
|
public boolean isInvisibilitySupported() {
|
|
SemanticVersion version = Utils.getMCVersion();
|
|
return version.getMajor() >= 1 && version.getMinor() >= 16;
|
|
}
|
|
|
|
public boolean isUpDownFaceSupported() {
|
|
SemanticVersion version = Utils.getMCVersion();
|
|
|
|
if (version.getMajor() < 1)
|
|
return false;
|
|
if (version.getMajor() == 1 && version.getMinor() == 14 && version.getRevision() >= 4)
|
|
return true;
|
|
return version.getMinor() > 14;
|
|
}
|
|
|
|
private void saveMaps() {
|
|
FileConfiguration config = new YamlConfiguration();
|
|
config.set(CONFIG_VERSION_KEY, CONFIG_VERSION);
|
|
config.set("maps", maps.entrySet().stream().collect(Collectors.toMap(Entry::getValue, Entry::getKey)));
|
|
|
|
BukkitRunnable saveTask = new LambdaRunnable(() -> {
|
|
try {
|
|
config.save(new File(getDataFolder(), "maps.yml"));
|
|
}
|
|
catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
});
|
|
|
|
if (isEnabled())
|
|
saveTask.runTaskAsynchronously(this);
|
|
else
|
|
saveTask.run();
|
|
}
|
|
|
|
private void loadMaps() {
|
|
Configuration config = YamlConfiguration.loadConfiguration(new File(getDataFolder(), "maps.yml"));
|
|
int version = config.getInt(CONFIG_VERSION_KEY, -1);
|
|
|
|
if (version == -1)
|
|
config = convertLegacyMaps(config);
|
|
|
|
ConfigurationSection section = config.getConfigurationSection("maps");
|
|
if (section != null)
|
|
section.getValues(false).forEach((a, b) -> {
|
|
int id = Integer.parseInt(a);
|
|
ImageMap imageMap = (ImageMap) b;
|
|
@SuppressWarnings("deprecation")
|
|
MapView map = Bukkit.getMap(id);
|
|
BufferedImage image = getImage(imageMap.getFilename());
|
|
|
|
if (image == null) {
|
|
getLogger().warning(() -> "Image file " + image + " not found. Removing map!");
|
|
return;
|
|
}
|
|
|
|
map.addRenderer(new ImageMapRenderer(image, imageMap.getX(), imageMap.getY(), imageMap.getScale()));
|
|
maps.put(imageMap, id);
|
|
});
|
|
}
|
|
|
|
private Configuration convertLegacyMaps(Configuration config) {
|
|
getLogger().info("Converting maps from Version <1.0");
|
|
|
|
Map<Integer, ImageMap> map = new HashMap<>();
|
|
|
|
for (String key : config.getKeys(false)) {
|
|
int id = Integer.parseInt(key);
|
|
String image = config.getString(key + ".image");
|
|
int x = config.getInt(key + ".x") / MAP_WIDTH;
|
|
int y = config.getInt(key + ".y") / MAP_HEIGHT;
|
|
double scale = config.getDouble(key + ".scale", 1.0);
|
|
map.put(id, new ImageMap(image, x, y, scale));
|
|
}
|
|
|
|
config = new YamlConfiguration();
|
|
config.set(CONFIG_VERSION_KEY, CONFIG_VERSION);
|
|
config.createSection("maps", map);
|
|
return config;
|
|
}
|
|
|
|
public boolean hasImage(String filename) {
|
|
if (imageCache.containsKey(filename.toLowerCase()))
|
|
return true;
|
|
|
|
File file = new File(getDataFolder(), IMAGES_DIR + File.separatorChar + filename);
|
|
|
|
return file.exists() && getImage(filename) != null;
|
|
}
|
|
|
|
public BufferedImage getImage(String filename) {
|
|
if (filename.contains("/") || filename.contains("\\") || filename.contains(":")) {
|
|
getLogger().warning("Someone tried to get image with illegal characters in file name.");
|
|
return null;
|
|
}
|
|
|
|
if (imageCache.containsKey(filename.toLowerCase()))
|
|
return imageCache.get(filename.toLowerCase());
|
|
|
|
File file = new File(getDataFolder(), IMAGES_DIR + File.separatorChar + filename);
|
|
BufferedImage image = null;
|
|
|
|
if (!file.exists())
|
|
return null;
|
|
|
|
try {
|
|
image = ImageIO.read(file);
|
|
imageCache.put(filename.toLowerCase(), image);
|
|
}
|
|
catch (IOException e) {
|
|
getLogger().log(Level.SEVERE, String.format("Error while trying to read image %s.", file.getName()), e);
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
@EventHandler
|
|
public void onInteract(PlayerInteractEvent event) {
|
|
Player player = event.getPlayer();
|
|
|
|
if (!player.hasMetadata(PLACEMENT_METADATA))
|
|
return;
|
|
|
|
if (event.getAction() == Action.RIGHT_CLICK_AIR) {
|
|
player.removeMetadata(PLACEMENT_METADATA, this);
|
|
MessageUtil.sendMessage(this, player, MessageLevel.NORMAL, "Image placement cancelled.");
|
|
return;
|
|
}
|
|
|
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK)
|
|
return;
|
|
|
|
PlacementData data = (PlacementData) player.getMetadata(PLACEMENT_METADATA).get(0).value();
|
|
PlacementResult result = placeImage(player, event.getClickedBlock(), event.getBlockFace(), data);
|
|
|
|
switch (result) {
|
|
case INVALID_FACING:
|
|
MessageUtil.sendMessage(this, player, MessageLevel.WARNING, "You can't place an image on this block face.");
|
|
break;
|
|
case INVALID_DIRECTION:
|
|
MessageUtil.sendMessage(this, player, MessageLevel.WARNING, "Couldn't calculate how to place the map.");
|
|
break;
|
|
case EVENT_CANCELLED:
|
|
MessageUtil.sendMessage(this, player, MessageLevel.NORMAL, "Image placement cancelled by another plugin.");
|
|
break;
|
|
case INSUFFICIENT_SPACE:
|
|
MessageUtil.sendMessage(this, player, MessageLevel.NORMAL, "Map couldn't be placed, the space is blocked.");
|
|
break;
|
|
case INSUFFICIENT_WALL:
|
|
MessageUtil.sendMessage(this, player, MessageLevel.NORMAL, "Map couldn't be placed, the supporting wall is too small.");
|
|
break;
|
|
case OVERLAPPING_ENTITY:
|
|
MessageUtil.sendMessage(this, player, MessageLevel.NORMAL, "Map couldn't be placed, there is another entity in the way.");
|
|
break;
|
|
case SUCCESS:
|
|
break;
|
|
}
|
|
|
|
player.removeMetadata(PLACEMENT_METADATA, this);
|
|
event.setCancelled(true);
|
|
}
|
|
|
|
private PlacementResult placeImage(Player player, Block block, BlockFace face, PlacementData data) {
|
|
if (!isAxisAligned(face)) {
|
|
getLogger().severe("Someone tried to create an image with an invalid block facing");
|
|
return PlacementResult.INVALID_FACING;
|
|
}
|
|
|
|
if (face.getModY() != 0 && !isUpDownFaceSupported())
|
|
return PlacementResult.INVALID_FACING;
|
|
|
|
Block b = block.getRelative(face);
|
|
BufferedImage image = getImage(data.getFilename());
|
|
Tuple<Integer, Integer> size = getImageSize(data.getFilename(), data.getSize());
|
|
BlockFace widthDirection = calculateWidthDirection(player, face);
|
|
BlockFace heightDirection = calculateHeightDirection(player, face);
|
|
|
|
if (widthDirection == null || heightDirection == null)
|
|
return PlacementResult.INVALID_DIRECTION;
|
|
|
|
// check for space
|
|
for (int x = 0; x < size.getKey(); x++)
|
|
for (int y = 0; y < size.getValue(); y++) {
|
|
Block frameBlock = b.getRelative(widthDirection, x).getRelative(heightDirection, y);
|
|
|
|
if (!block.getRelative(widthDirection, x).getRelative(heightDirection, y).getType().isSolid())
|
|
return PlacementResult.INSUFFICIENT_WALL;
|
|
if (frameBlock.getType().isSolid())
|
|
return PlacementResult.INSUFFICIENT_SPACE;
|
|
if (!b.getWorld().getNearbyEntities(frameBlock.getLocation().add(0.5, 0.5, 0.5), 0.5, 0.5, 0.5, a -> (a instanceof Hanging)).isEmpty())
|
|
return PlacementResult.OVERLAPPING_ENTITY;
|
|
}
|
|
|
|
ImagePlaceEvent event = new ImagePlaceEvent(player, block, widthDirection, heightDirection, size.getKey(), size.getValue(), data);
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
if (event.isCancelled())
|
|
return PlacementResult.EVENT_CANCELLED;
|
|
|
|
// spawn item frame
|
|
for (int x = 0; x < size.getKey(); x++)
|
|
for (int y = 0; y < size.getValue(); y++) {
|
|
ItemFrame frame = block.getWorld().spawn(b.getRelative(widthDirection, x).getRelative(heightDirection, y).getLocation(), ItemFrame.class);
|
|
frame.setFacingDirection(face);
|
|
frame.setItem(getMapItem(image, x, y, data));
|
|
frame.setRotation(facingToRotation(heightDirection, widthDirection));
|
|
|
|
if (isInvisibilitySupported()) {
|
|
frame.setFixed(data.isFixed());
|
|
frame.setVisible(!data.isInvisible());
|
|
}
|
|
}
|
|
|
|
return PlacementResult.SUCCESS;
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
public boolean reloadImage(String filename) {
|
|
if (!imageCache.containsKey(filename.toLowerCase()))
|
|
return false;
|
|
|
|
imageCache.remove(filename.toLowerCase());
|
|
BufferedImage image = getImage(filename);
|
|
|
|
if (image == null) {
|
|
getLogger().warning(() -> "Failed to reload image: " + filename);
|
|
return false;
|
|
}
|
|
|
|
maps.entrySet().stream().filter(a -> a.getKey().getFilename().equalsIgnoreCase(filename)).map(a -> Bukkit.getMap(a.getValue()))
|
|
.flatMap(a -> a.getRenderers().stream()).filter(a -> a instanceof ImageMapRenderer).forEach(a -> ((ImageMapRenderer) a).recalculateInput(image));
|
|
return true;
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
private ItemStack getMapItem(BufferedImage image, int x, int y, PlacementData data) {
|
|
ItemStack item = new ItemStack(Material.FILLED_MAP);
|
|
|
|
ImageMap imageMap = new ImageMap(data.getFilename(), x, y, getScale(image, data.getSize()));
|
|
if (maps.containsKey(imageMap)) {
|
|
MapMeta meta = (MapMeta) item.getItemMeta();
|
|
meta.setMapId(maps.get(imageMap));
|
|
item.setItemMeta(meta);
|
|
return item;
|
|
}
|
|
|
|
MapView map = getServer().createMap(getServer().getWorlds().get(0));
|
|
map.getRenderers().forEach(map::removeRenderer);
|
|
map.addRenderer(new ImageMapRenderer(image, x, y, getScale(image, data.getSize())));
|
|
|
|
MapMeta meta = ((MapMeta) item.getItemMeta());
|
|
meta.setMapView(map);
|
|
item.setItemMeta(meta);
|
|
maps.put(imageMap, map.getId());
|
|
|
|
return item;
|
|
}
|
|
|
|
public Tuple<Integer, Integer> getImageSize(String filename, Tuple<Integer, Integer> size) {
|
|
BufferedImage image = getImage(filename);
|
|
|
|
if (image == null)
|
|
return new Tuple<>(0, 0);
|
|
|
|
double finalScale = getScale(image, size);
|
|
int finalX = (int) ((MAP_WIDTH - 1 + Math.ceil(image.getWidth() * finalScale)) / MAP_WIDTH);
|
|
int finalY = (int) ((MAP_HEIGHT - 1 + Math.ceil(image.getHeight() * finalScale)) / MAP_HEIGHT);
|
|
|
|
return new Tuple<>(finalX, finalY);
|
|
}
|
|
|
|
public double getScale(String filename, Tuple<Integer, Integer> size) {
|
|
return getScale(getImage(filename), size);
|
|
}
|
|
|
|
public double getScale(BufferedImage image, Tuple<Integer, Integer> size) {
|
|
if (image == null)
|
|
return 1.0;
|
|
|
|
int baseX = image.getWidth();
|
|
int baseY = image.getHeight();
|
|
|
|
double finalScale = 1D;
|
|
|
|
if (size != null) {
|
|
int targetX = size.getKey() * MAP_WIDTH;
|
|
int targetY = size.getValue() * MAP_HEIGHT;
|
|
|
|
double scaleX = size.getKey() > 0 ? (double) targetX / baseX : Double.MAX_VALUE;
|
|
double scaleY = size.getValue() > 0 ? (double) targetY / baseY : Double.MAX_VALUE;
|
|
|
|
finalScale = Math.min(scaleX, scaleY);
|
|
if (finalScale >= Double.MAX_VALUE)
|
|
finalScale = 1D;
|
|
}
|
|
|
|
return finalScale;
|
|
}
|
|
|
|
private static Rotation facingToRotation(BlockFace heightDirection, BlockFace widthDirection) {
|
|
switch (heightDirection) {
|
|
case WEST:
|
|
return Rotation.CLOCKWISE_45;
|
|
case NORTH:
|
|
return widthDirection == BlockFace.WEST ? Rotation.CLOCKWISE : Rotation.NONE;
|
|
case EAST:
|
|
return Rotation.CLOCKWISE_135;
|
|
case SOUTH:
|
|
return widthDirection == BlockFace.WEST ? Rotation.CLOCKWISE : Rotation.NONE;
|
|
default:
|
|
return Rotation.NONE;
|
|
}
|
|
}
|
|
|
|
private static BlockFace calculateWidthDirection(Player player, BlockFace face) {
|
|
float yaw = (360.0f + player.getLocation().getYaw()) % 360.0f;
|
|
switch (face) {
|
|
case NORTH:
|
|
return BlockFace.WEST;
|
|
case SOUTH:
|
|
return BlockFace.EAST;
|
|
case EAST:
|
|
return BlockFace.NORTH;
|
|
case WEST:
|
|
return BlockFace.SOUTH;
|
|
case UP:
|
|
case DOWN:
|
|
if (Utils.isBetween(yaw, 45.0, 135.0))
|
|
return BlockFace.NORTH;
|
|
else if (Utils.isBetween(yaw, 135.0, 225.0))
|
|
return BlockFace.EAST;
|
|
else if (Utils.isBetween(yaw, 225.0, 315.0))
|
|
return BlockFace.SOUTH;
|
|
else
|
|
return BlockFace.WEST;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static BlockFace calculateHeightDirection(Player player, BlockFace face) {
|
|
float yaw = (360.0f + player.getLocation().getYaw()) % 360.0f;
|
|
switch (face) {
|
|
case NORTH:
|
|
case SOUTH:
|
|
case EAST:
|
|
case WEST:
|
|
return BlockFace.DOWN;
|
|
case UP:
|
|
if (Utils.isBetween(yaw, 45.0, 135.0))
|
|
return BlockFace.EAST;
|
|
else if (Utils.isBetween(yaw, 135.0, 225.0))
|
|
return BlockFace.SOUTH;
|
|
else if (Utils.isBetween(yaw, 225.0, 315.0))
|
|
return BlockFace.WEST;
|
|
else
|
|
return BlockFace.NORTH;
|
|
case DOWN:
|
|
if (Utils.isBetween(yaw, 45.0, 135.0))
|
|
return BlockFace.WEST;
|
|
else if (Utils.isBetween(yaw, 135.0, 225.0))
|
|
return BlockFace.NORTH;
|
|
else if (Utils.isBetween(yaw, 225.0, 315.0))
|
|
return BlockFace.EAST;
|
|
else
|
|
return BlockFace.SOUTH;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static boolean isAxisAligned(BlockFace face) {
|
|
switch (face) {
|
|
case DOWN:
|
|
case UP:
|
|
case WEST:
|
|
case EAST:
|
|
case SOUTH:
|
|
case NORTH:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|