New Sign-based Prompt GUI API.

* NEW: Added new Callback class.
* NEW: Added ReflectionUtils module.
* NEW: Added new PromptGui class.
* NEW: MapDetailGui: Implement renaming using new prompt API.
* BUG: ExplorerGUI: Fix a crash when setting null data.
* BUG: InventoryGUI: Fix titles longer than 32 characters.
This commit is contained in:
Adrien Prokopowicz 2015-09-28 16:09:04 +02:00
parent 48dbdbd9d4
commit 96093898c8
6 changed files with 445 additions and 3 deletions

View File

@ -0,0 +1,134 @@
/*
* Copyright (C) 2013 Moribus
* Copyright (C) 2015 ProkopyL <prokopylmc@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2013 Moribus
* Copyright (C) 2015 ProkopyL <prokopylmc@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package fr.moribus.imageonmap.guiproko.core;
public interface Callback<T>
{
public void call(T parameter);
}

View File

@ -98,10 +98,11 @@ abstract public class ExplorerGui<T> 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);
}
/**

View File

@ -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; }

View File

@ -0,0 +1,257 @@
/*
* Copyright (C) 2013 Moribus
* Copyright (C) 2015 ProkopyL <prokopylmc@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> callback)
{
prompt(owner, callback, "");
}
static public void prompt(Player owner, Callback<String> 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<String> callback;
private Location signLocation;
private String contents;
public PromptGui(Callback<String> callback, String contents)
{
this(callback);
this.contents = contents;
}
public PromptGui(Callback<String> 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());
}
}
}

View File

@ -68,6 +68,25 @@ public class MapDetailGui extends ExplorerGui<Void>
else return super.getEmptyViewItem();
}
@GuiAction
private void rename()
{
PromptGui.prompt(getPlayer(), new Callback<String>()
{
@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()
{