diff --git a/src/main/java/fr/moribus/imageonmap/ReflectionUtils.java b/src/main/java/fr/moribus/imageonmap/ReflectionUtils.java new file mode 100644 index 0000000..2775844 --- /dev/null +++ b/src/main/java/fr/moribus/imageonmap/ReflectionUtils.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 Moribus + * Copyright (C) 2015 ProkopyL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package fr.moribus.imageonmap; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.bukkit.Bukkit; + +abstract public class ReflectionUtils +{ + static public String getBukkitPackageVersion() + { + return getBukkitPackageName().substring("org.bukkit.craftbukkit.".length()); + } + + static public String getBukkitPackageName() + { + return Bukkit.getServer().getClass().getPackage().getName(); + } + + static public String getMinecraftPackageName() + { + return "net.minecraft.server." + getBukkitPackageVersion(); + } + + static public Class getBukkitClassByName(String name) throws ClassNotFoundException + { + return Class.forName(getBukkitPackageName() + "." + name); + } + + static public Class getMinecraftClassByName(String name) throws ClassNotFoundException + { + return Class.forName(getMinecraftPackageName() + "." + name); + } + + static public Object getFieldValue(Class hClass, Object instance, String name) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException + { + return getField(hClass, name).get(instance); + } + + static public Object getFieldValue(Object instance, String name) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException + { + return getFieldValue(instance.getClass(), instance, name); + } + + static public Field getField(Class klass, String name) throws NoSuchFieldException + { + Field field = klass.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + static public Field getField(Class klass, Class type) throws NoSuchFieldException + { + for(Field field : klass.getDeclaredFields()) + { + if(field.getType().equals(type)) + { + field.setAccessible(true); + return field; + } + } + throw new NoSuchFieldException("Class " + klass.getName() + " does not define any field of type " + type.getName()); + } + + static public void setFieldValue(Object instance, String name, Object value) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException + { + setFieldValue(instance.getClass(), instance, name, value); + } + + static public void setFieldValue(Class hClass, Object instance, String name, Object value) + throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException + { + getField(hClass, name).set(instance, value); + } + + static public Object call(Class hClass, String name, Object ... parameters) + throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + return call(hClass, null, name, parameters); + } + + static public Object call(Object instance, String name, Object ... parameters) + throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + return call(instance.getClass(), instance, name, parameters); + } + + static public Object call(Class hClass, Object instance, String name, Object ... parameters) + throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + Method method = hClass.getMethod(name, getTypes(parameters)); + return method.invoke(instance, parameters); + } + + static public Object instanciate(Class hClass, Object ... parameters) + throws NoSuchMethodException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException + { + Constructor constructor = hClass.getConstructor(getTypes(parameters)); + return constructor.newInstance(parameters); + } + + static public Class[] getTypes(Object[] objects) + { + Class[] types = new Class[objects.length]; + for(int i = 0; i < objects.length; i++) + { + types[i] = objects[i].getClass(); + } + return types; + } +} \ No newline at end of file diff --git a/src/main/java/fr/moribus/imageonmap/guiproko/core/Callback.java b/src/main/java/fr/moribus/imageonmap/guiproko/core/Callback.java new file mode 100644 index 0000000..3b53865 --- /dev/null +++ b/src/main/java/fr/moribus/imageonmap/guiproko/core/Callback.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2013 Moribus + * Copyright (C) 2015 ProkopyL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.moribus.imageonmap.guiproko.core; + +public interface Callback +{ + public void call(T parameter); +} diff --git a/src/main/java/fr/moribus/imageonmap/guiproko/core/ExplorerGui.java b/src/main/java/fr/moribus/imageonmap/guiproko/core/ExplorerGui.java index bddad13..be2c4d6 100644 --- a/src/main/java/fr/moribus/imageonmap/guiproko/core/ExplorerGui.java +++ b/src/main/java/fr/moribus/imageonmap/guiproko/core/ExplorerGui.java @@ -98,10 +98,11 @@ abstract public class ExplorerGui extends ActionGui protected void setData(T[] data, int dataWidth) { this.data = data; + int dataLength = data == null ? 0 : data.length; if(dataWidth > 0) - setDataShape(dataWidth, (int) Math.ceil((double) data.length / (double) dataWidth)); + setDataShape(dataWidth, (int) Math.ceil((double) dataLength / (double) dataWidth)); else - setDataShape(0, data.length); + setDataShape(0, dataLength); } /** diff --git a/src/main/java/fr/moribus/imageonmap/guiproko/core/InventoryGui.java b/src/main/java/fr/moribus/imageonmap/guiproko/core/InventoryGui.java index f2c88f0..b42285a 100644 --- a/src/main/java/fr/moribus/imageonmap/guiproko/core/InventoryGui.java +++ b/src/main/java/fr/moribus/imageonmap/guiproko/core/InventoryGui.java @@ -35,6 +35,7 @@ abstract public class InventoryGui extends Gui static protected final int INVENTORY_ROW_SIZE = 9; static protected final int MAX_INVENTORY_COLUMN_SIZE = 6; static protected final int MAX_INVENTORY_SIZE = INVENTORY_ROW_SIZE * MAX_INVENTORY_COLUMN_SIZE; + static protected final int MAX_TITLE_LENGTH = 32; public InventoryGui() { @@ -197,7 +198,14 @@ abstract public class InventoryGui extends Gui * It will be applied on the next GUI update. * @param title The new title of the inventory */ - protected void setTitle(String title){this.title = title;} + protected void setTitle(String title) + { + if(title != null && title.length() > MAX_TITLE_LENGTH) + { + title = title.substring(0, MAX_TITLE_LENGTH - 4) + "..."; + } + this.title = title; + } /** @return The underlying inventory, or null if the Gui has not been opened yet. */ public Inventory getInventory() { return inventory; } diff --git a/src/main/java/fr/moribus/imageonmap/guiproko/core/PromptGui.java b/src/main/java/fr/moribus/imageonmap/guiproko/core/PromptGui.java new file mode 100644 index 0000000..cb13875 --- /dev/null +++ b/src/main/java/fr/moribus/imageonmap/guiproko/core/PromptGui.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2013 Moribus + * Copyright (C) 2015 ProkopyL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package fr.moribus.imageonmap.guiproko.core; + +import fr.moribus.imageonmap.ImageOnMap; +import fr.moribus.imageonmap.PluginLogger; +import fr.moribus.imageonmap.ReflectionUtils; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.SignChangeEvent; + +public class PromptGui extends Gui +{ + static private final int SIGN_LINES_COUNT = 4; + static private final int SIGN_COLUMNS_COUNT = 15; + + static private boolean isInitialized = false; + + /* ===== Reflection to Sign API ===== */ + static private Field fieldSign = null;//CraftSign.sign + static private Field fieldIsEditable = null;//TileEntitySign.isEditable + static private Method methodGetHandle = null;//CraftPlayer.getHandle() + static private Method methodOpenSign = null;//EntityHuman.openSign() + static private Field fieldSignEditor = null;//TileEntitySign.k{EntityHuman} + static private Class classTileEntitySign = null;//CraftBlock.class + + static public boolean isAvailable() + { + if(!isInitialized) init(); + return fieldSign != null; + } + + static public void prompt(Player owner, Callback callback) + { + prompt(owner, callback, ""); + } + + static public void prompt(Player owner, Callback callback, String contents) + { + Gui.open(owner, new PromptGui(callback, contents)); + } + + static private void init() + { + isInitialized = true; + + try + { + Class CraftSign = ReflectionUtils.getBukkitClassByName("block.CraftSign"); + classTileEntitySign = ReflectionUtils.getMinecraftClassByName("TileEntitySign"); + Class CraftPlayer = ReflectionUtils.getBukkitClassByName("entity.CraftPlayer"); + Class EntityHuman = ReflectionUtils.getMinecraftClassByName("EntityHuman"); + + + fieldSign = ReflectionUtils.getField(CraftSign, "sign"); + fieldIsEditable = ReflectionUtils.getField(classTileEntitySign, "isEditable"); + methodGetHandle = CraftPlayer.getDeclaredMethod("getHandle"); + methodOpenSign = EntityHuman.getDeclaredMethod("openSign", classTileEntitySign); + fieldSignEditor = ReflectionUtils.getField(classTileEntitySign, EntityHuman); + } + catch (Exception ex) + { + PluginLogger.error("Unable to initialize Sign Prompt API", ex); + fieldSign = null; + } + } + + private final Callback callback; + private Location signLocation; + private String contents; + + public PromptGui(Callback callback, String contents) + { + this(callback); + this.contents = contents; + } + + public PromptGui(Callback callback) + { + super(); + registerListener(PromptGuiListener.class); + if(!isAvailable()) throw new IllegalStateException("Sign-based prompt GUI are not available"); + this.callback = callback; + } + + @Override + protected void open(final Player player) + { + super.open(player); + + signLocation = findAvailableLocation(player); + Block block = player.getWorld().getBlockAt(signLocation); + block.setType(Material.SIGN_POST, false); + final Sign sign = (Sign) block.getState(); + setSignContents(sign, contents); + sign.update(); + + Bukkit.getScheduler().scheduleSyncDelayedTask(ImageOnMap.getPlugin(), new Runnable() + { + @Override + public void run() + { + try + { + Object signTE = fieldSign.get(sign); + Object playerEntity = methodGetHandle.invoke(player); + methodOpenSign.invoke(playerEntity, signTE); + } + catch(Throwable ex) + { + PluginLogger.error("Error while opening Sign prompt", ex); + } + } + }, 3); + + } + + @Override + public void close() + { + Block block = getPlayer().getWorld().getBlockAt(signLocation); + block.setType(Material.AIR); + super.close(); + } + + private void validate(String[] lines) + { + callback.call(getSignContents(lines)); + this.close(); + } + + static private String getSignContents(String[] lines) + { + String content = lines[0].trim(); + + for(int i = 1; i < lines.length; i++) + { + if(lines[i] == null || lines[i].isEmpty()) continue; + content += " " + lines[i].trim(); + } + return content.trim(); + } + + static private void setSignContents(Sign sign, String content) + { + String[] lines = new String[SIGN_LINES_COUNT + 1]; + String curLine; + int curLineIndex = 0, spacePos; + + if(content != null) + { + lines[0] = content; + while(curLineIndex < SIGN_LINES_COUNT) + { + curLine = lines[curLineIndex]; + if(curLine.length() <= SIGN_COLUMNS_COUNT) + break; + + spacePos = curLine.lastIndexOf(' ', SIGN_COLUMNS_COUNT); + if(spacePos < 0) break; + lines[curLineIndex + 1] = curLine.substring(spacePos + 1); + lines[curLineIndex] = curLine.substring(0, spacePos); + curLineIndex++; + } + } + + for(int i = SIGN_LINES_COUNT; i --> 0;) + { + sign.setLine(i, lines[i]); + } + } + + static private Location findAvailableLocation(Player player) + { + World world = player.getWorld(); + Chunk playerChunk = player.getLocation().getChunk(); + Chunk firstChunk = world.getChunkAt(playerChunk.getX() - 1, playerChunk.getZ() - 1); + Location firstLoc = firstChunk.getBlock(0, 255, 0).getLocation(); + Location loc; + + for(int i = 48; i --> 0;) + { + for(int j = 0; j --> -10;) + { + for(int k = 48; k --> 0;) + { + loc = firstLoc.add(i, j, k); + if(hasSpace(world, loc)) + return loc; + } + } + } + + return null; + } + + static private boolean hasSpace(World world, Location loc) + { + if(!Material.AIR.equals(world.getBlockAt(loc).getType())) + return false; + + for(int i = 1; i --> -1;) + { + for(int j = 1; j --> -1;) + { + for(int k = 1; k --> -1;) + { + if(!Material.AIR.equals(world.getBlockAt( + loc.getBlockX() + i, loc.getBlockY() + j, loc.getBlockZ() + k) + .getType())) + return false; + } + } + } + + return true; + } + + static private final class PromptGuiListener implements Listener + { + @EventHandler + public void onSignChange(SignChangeEvent event) + { + PromptGui gui = Gui.getOpenGui(event.getPlayer(), PromptGui.class); + if(gui == null) return; + gui.validate(event.getLines()); + } + } +} diff --git a/src/main/java/fr/moribus/imageonmap/guiproko/list/MapDetailGui.java b/src/main/java/fr/moribus/imageonmap/guiproko/list/MapDetailGui.java index 667b252..e11d4a7 100644 --- a/src/main/java/fr/moribus/imageonmap/guiproko/list/MapDetailGui.java +++ b/src/main/java/fr/moribus/imageonmap/guiproko/list/MapDetailGui.java @@ -68,6 +68,25 @@ public class MapDetailGui extends ExplorerGui else return super.getEmptyViewItem(); } + @GuiAction + private void rename() + { + PromptGui.prompt(getPlayer(), new Callback() + { + @Override + public void call(String newName) + { + if(newName == null || newName.isEmpty()) + { + getPlayer().sendMessage(ChatColor.RED + "Map names can't be empty."); + return; + } + map.rename(newName); + getPlayer().sendMessage(ChatColor.GRAY + "Map successfuly renamed."); + } + }, map.getName()); + } + @GuiAction private void delete() {