package me.desht.scrollingmenusign.views; import me.desht.dhutils.Debugger; import me.desht.dhutils.LogUtils; import me.desht.dhutils.block.BlockUtil; import me.desht.scrollingmenusign.*; import me.desht.scrollingmenusign.views.SMSMapView.SMSMapRenderer; import me.desht.scrollingmenusign.views.SMSView.MenuStack; import me.desht.scrollingmenusign.views.action.RepaintAction; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Entity; import org.bukkit.entity.ItemFrame; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.map.MapRenderer; import org.bukkit.map.MapView; import org.bukkit.util.Vector; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.Map.Entry; public class ViewManager { // map view name to view object for registered views private final Map<String, SMSView> allViewNames = new HashMap<String, SMSView>(); private final ScrollingMenuSign plugin; public ViewManager(ScrollingMenuSign plugin) { this.plugin = plugin; } /** * Instantiate a new view from a saved config file * * @param node The configuration * @return The view object */ public SMSView loadView(ConfigurationSection node) { String viewName = null; try { SMSPersistence.mustHaveField(node, "class"); SMSPersistence.mustHaveField(node, "name"); SMSPersistence.mustHaveField(node, "menu"); String className = node.getString("class"); viewName = node.getString("name"); Class<? extends SMSView> c = Class.forName(className).asSubclass(SMSView.class); Constructor<? extends SMSView> ctor = c.getDeclaredConstructor(String.class, SMSMenu.class); SMSMenu menu = ScrollingMenuSign.getInstance().getMenuManager().getMenu(node.getString("menu")); SMSView view = ctor.newInstance(viewName, menu); if (!node.contains("group") && node.getString(SMSView.ACCESS).equals("GROUP")) { // migration - GROUP access becomes OWNER_GROUP in v3.0.0+ LogUtils.info("view " + viewName + ": migrate GROUP -> OWNER_GROUP access"); node.set(SMSView.ACCESS, "OWNER_GROUP"); } view.thaw(node); registerView(view); return view; } catch (ClassNotFoundException e) { loadError(viewName, e); } catch (SMSException e) { loadError(viewName, e); } catch (InstantiationException e) { loadError(viewName, e); } catch (IllegalAccessException e) { loadError(viewName, e); } catch (SecurityException e) { loadError(viewName, e); } catch (NoSuchMethodException e) { loadError(viewName, e); } catch (IllegalArgumentException e) { loadError(viewName, e); } catch (InvocationTargetException e) { loadError(viewName, e.getCause()); } return null; } private void loadError(String viewName, Throwable e) { LogUtils.warning("Caught " + e.getClass().getName() + " while loading view " + viewName); LogUtils.warning(" Exception message: " + e.getMessage()); } /** * Register this view in the global view list and get it saved to disk. */ public void registerView(SMSView view) { if (allViewNames.containsKey(view.getName())) { throw new SMSException("A view named '" + view.getName() + "' already exists."); } allViewNames.put(view.getName(), view); for (Location l : view.getLocations()) { plugin.getLocationManager().registerLocation(l, view); } view.getNativeMenu().addObserver(view); view.autosave(); } /** * Unregister a view: remove it as an observer from its menu (and any active submenus), * and remove its name & location(s) from the manager. * * @param view the view to unregister */ private void unregisterView(SMSView view) { view.getNativeMenu().deleteObserver(view); for (UUID playerId : view.getSubmenuPlayers()) { MenuStack mst = view.getMenuStack(playerId); for (WeakReference<SMSMenu> ref : mst.stack) { SMSMenu m = ref.get(); if (m != null) { m.deleteObserver(view); } } } allViewNames.remove(view.getName()); for (Location l : view.getLocations()) { plugin.getLocationManager().unregisterLocation(l); } } public void deleteView(SMSView view, boolean permanent) { unregisterView(view); view.onDeleted(permanent); if (permanent) { SMSPersistence.unPersist(view); } } public void registerLocation(Location loc, SMSView view) { plugin.getLocationManager().registerLocation(loc, view); } public void unregisterLocation(Location loc) { plugin.getLocationManager().unregisterLocation(loc); } /** * Check to see if the name view exists * * @param name The view name * @return true if the named view exists, false otherwise */ public boolean checkForView(String name) { return allViewNames.containsKey(name); } /** * Get all known view objects as a List * * @return A list of all known views */ public List<SMSView> listViews() { return new ArrayList<SMSView>(allViewNames.values()); } /** * Get all known view objects as a Java array * * @return An array of all known views */ public SMSView[] getViewsAsArray() { return allViewNames.values().toArray(new SMSView[allViewNames.size()]); } /** * Get the named SMSView object * * @param name The view name * @return The SMSView object of that name * @throws SMSException if there is no such view with the given name */ public SMSView getView(String name) throws SMSException { if (!checkForView(name)) throw new SMSException("No such view: " + name); return allViewNames.get(name); } /** * Get the view object at the given location, if any. * * @param loc The location to check * @return The SMSView object at that location, or null if there is none */ public SMSView getViewForLocation(Location loc) { return plugin.getLocationManager().getInteractableAt(loc, SMSView.class); } /** * Find all the views for the given menu. * * @param menu The menu object to check * @return A list of SMSView objects which are views for that menu */ public List<SMSView> getViewsForMenu(SMSMenu menu) { return getViewsForMenu(menu, false); } /** * Find all the views for the given menu, optionally sorting the resulting list. * * @param menu The menu object to check * @param isSorted If true, sort the returned view list by view name * @return A list of SMSView objects which are views for that menu */ public List<SMSView> getViewsForMenu(SMSMenu menu, boolean isSorted) { if (isSorted) { SortedSet<String> sorted = new TreeSet<String>(allViewNames.keySet()); List<SMSView> res = new ArrayList<SMSView>(); for (String name : sorted) { SMSView v = allViewNames.get(name); if (v.getNativeMenu() == menu) { res.add(v); } } return res; } else { return new ArrayList<SMSView>(allViewNames.values()); } } /** * Get a count of views used, keyed by view type. Used for metrics gathering. * * @return a map of type -> count of views of that type */ public Map<String, Integer> getViewCounts() { Map<String, Integer> map = new HashMap<String, Integer>(); for (Entry<String, SMSView> e : allViewNames.entrySet()) { String type = e.getValue().getType(); if (!map.containsKey(type)) { map.put(type, 1); } else { map.put(type, map.get(type) + 1); } } return map; } /** * Get the view that the player is looking at (or holding), if any. * * @param player The player * @param mustExist if true and no view is found, throw an exception * @return the view being looked at, or null if no view is targeted * @throws SMSException if mustExist is true and no view is targeted */ public SMSView getTargetedView(Player player, boolean mustExist) { SMSView view = null; if (player.getItemInHand().getType() == Material.MAP) { view = getHeldMapView(player); } if (view == null && PopupBook.holding(player)) { // popup book (spout/inventory/private-holo) PopupBook book = PopupBook.get(player); view = book.getView(); } ItemStack stack = player.getItemInHand(); if (view == null && stack != null && PopupItem.getPopupItemFields(stack.getItemMeta()) != null) { PopupItem item = PopupItem.get(stack); view = item.getView(); } Block b = null; if (view == null) { // targeted view (sign/multisign/redstone/public-holo) try { b = player.getTargetBlock(null, ScrollingMenuSign.BLOCK_TARGET_DIST); view = getViewForLocation(b.getLocation()); } catch (IllegalStateException e) { // the block iterator can throw this sometimes - we can ignore it } } if (view == null && b != null) { // maybe there's a map view item frame attached to the block we're looking at ItemFrame frame = findMapFrame(b, player.getEyeLocation()); if (frame != null) { view = getMapViewForId(frame.getItem().getDurability()); } } if (view == null && mustExist) { throw new SMSException("You are not looking at a menu view."); } return view; } /** * Get the view that the player is looking at (or holding), if any. * * @param player The player * @return the view being looked at, or null if no view is targeted */ public SMSView getTargetedView(Player player) { return getTargetedView(player, false); } /** * Find a view for the given menu. * * @param menu the SMS menu * @return the first view found for the menu */ public SMSView findView(SMSMenu menu) { return findView(menu, (Class<?>) null); } /** * Find a view for the given menu. * * @param menu the SMS menu * @param c if non-null, the view must be a instanceof of this class or a subclass * @return the first view found of the given class (or interface) for the menu */ public SMSView findView(SMSMenu menu, Class<?> c) { for (SMSView view : listViews()) { if (view.getNativeMenu() == menu && (c == null || c.isAssignableFrom(view.getClass()))) { return view; } } return null; } /** * Find a view for the given menu. * * @param menu the SMS menu * @param type if non-null, the desired view type as returned * by {@link me.desht.scrollingmenusign.views.SMSView#getType()} * @return the first view found of the given class (or interface) for the menu */ public SMSView findView(SMSMenu menu, String type) { for (SMSView view : listViews()) { if (view.getNativeMenu() == menu && (type == null || type.equals(view.getType()))) { return view; } } return null; } /** * Called when a player logs out. Call the clearPlayerForView() method on all * known views. * * @param player the player object */ public void clearPlayer(Player player) { for (SMSView v : listViews()) { v.clearPlayerForView(player); } } /** * Load any deferred locations for the given world. This is called by the WorldLoadEvent handler. * * @param world The world that's just been loaded. */ public void loadDeferred(World world) { for (SMSView view : listViews()) { List<Vector> l = view.getDeferredLocations(world.getName()); if (l == null) { continue; } for (Vector vec : l) { try { view.addLocation(new Location(world, vec.getBlockX(), vec.getBlockY(), vec.getBlockZ())); Debugger.getInstance().debug("added loc " + world.getName() + ", " + vec + " to view " + view.getName()); } catch (SMSException e) { LogUtils.warning("Can't add location " + world.getName() + ", " + vec + " to view " + view.getName()); LogUtils.warning(" Exception message: " + e.getMessage()); } } l.clear(); } } /** * Given a map ID, return the map view object for that ID, if any. * * @param mapId The ID of the map * @return The SMSMapView object for the ID, or null if this map ID isn't used for a SMSMapView */ public SMSMapView getMapViewForId(short mapId) { MapView mv = Bukkit.getMap(mapId); if (mv != null) { for (MapRenderer r : mv.getRenderers()) { if (r instanceof SMSMapRenderer) { return ((SMSMapRenderer) r).getView(); } } } return null; } /** * Check if the given map ID is used for a SMSMapView * * @param mapId The ID of the map * @return true if the ID is used for a SMSMapView, false otherwise */ public boolean checkForMapId(short mapId) { MapView mv = Bukkit.getMap(mapId); if (mv != null) { for (MapRenderer r : mv.getRenderers()) { if (r instanceof SMSMapRenderer) { return true; } } } return false; } /** * Convenience routine. Add the given mapId as a view on the given menu. * * @param menu The menu to add the view to * @param mapId ID of the map that will be used as a view * @return The SMSMapView object that was just created * @throws SMSException if the given mapId is already a view */ public SMSMapView addMapToMenu(String viewName, SMSMenu menu, short mapId, CommandSender owner) throws SMSException { if (checkForMapId(mapId)) { throw new SMSException("Map #" + mapId + " already has a menu view associated with it"); } if (isMapUsedByOtherPlugin(mapId)) { throw new SMSException("Map #" + mapId + " is used by another plugin"); } SMSMapView mapView = new SMSMapView(viewName, menu); mapView.setMapId(mapId); initialiseView(mapView, owner); return mapView; } public SMSMapView addMapToMenu(SMSMenu menu, short mapId, CommandSender owner) throws SMSException { return addMapToMenu(null, menu, mapId, owner); } /** * Check to see if this map ID is used by another plugin, to avoid toe-stepping-upon... * The check is for any renderers on the map of a class outside the org.bukkit namespace. * * @param mapId ID of the map to check * @return true if it's used by someone else, false otherwise */ public boolean isMapUsedByOtherPlugin(short mapId) { MapView mapView = Bukkit.getServer().getMap(mapId); for (MapRenderer r : mapView.getRenderers()) { if (!r.getClass().getPackage().getName().startsWith("org.bukkit")) { return true; } } return false; } /** * Convenience routine. Get the map view that the player is holding, if any. * * @param player The player to check for * @return A SMSMapView object if the player is holding one, null otherwise */ public SMSMapView getHeldMapView(Player player) { if (player.getItemInHand().getType() == Material.MAP) { return getMapViewForId(player.getItemInHand().getDurability()); } else { return null; } } /** * Get the item frame attached to the given block, if any, on the * side of the block facing most directly toward the given location * (typically a player's eye location). * <p/> * The item frame must be holding a map which is a SMSMapView. * * @param block the block to check * @param viewerLoc the location to check from * @return the item frame object, or null if none was found */ public ItemFrame findMapFrame(Block block, Location viewerLoc) { BlockFace face = BlockUtil.getNearestFace(block, viewerLoc); for (Entity entity : block.getWorld().getEntitiesByClass(ItemFrame.class)) { ItemFrame frame = (ItemFrame) entity; if (frame.getItem() == null || frame.getItem().getType() != Material.MAP || !checkForMapId(frame.getItem().getDurability())) { continue; } if (frame.getLocation().getBlock().getRelative(frame.getAttachedFace()).equals(block) && frame.getAttachedFace() == face.getOppositeFace()) { return frame; } } return null; } /** * Convenience method. Create a new inventory view for the given menu. * * @param menu the menu to add the new view to * @param owner owner of the new view * @return the newly-created view */ public SMSInventoryView addInventoryViewToMenu(SMSMenu menu, CommandSender owner) { return addInventoryViewToMenu(null, menu, owner); } public SMSInventoryView addInventoryViewToMenu(String viewName, SMSMenu menu, CommandSender owner) { SMSInventoryView view = new SMSInventoryView(viewName, menu); initialiseView(view, owner); return view; } /** * Convenience method. Create a new multi-sign view at the given location. * * @param viewName name for the new view * @param menu the menu to add the new view to * @param location location of one of the signs in the new view * @param owner owner of the new view * @return the newly-created view * @throws SMSException */ public SMSView addMultiSignToMenu(String viewName, SMSMenu menu, Location location, CommandSender owner) throws SMSException { SMSView view = new SMSMultiSignView(viewName, menu, location); initialiseView(view, owner); return view; } /** * Convenience method. Create a new multi-sign view at the given location. * * @param menu the menu to add the view to * @param location location of one of the signs in the view * @param owner owner of the new view * @return the newly-created view * @throws SMSException */ public SMSView addMultiSignToMenu(SMSMenu menu, Location location, CommandSender owner) throws SMSException { return addMultiSignToMenu(null, menu, location, owner); } /** * Convenience method. Create a new redstone view at the given location and add it * to the given menu. * * @param menu The menu to add the view to. * @param loc The location for the view. * @throws SMSException if the location is not suitable for this view */ public SMSView addRedstoneViewToMenu(String viewName, SMSMenu menu, Location loc, CommandSender owner) throws SMSException { SMSView view = new SMSRedstoneView(viewName, menu); initialiseView(view, owner); return view; } public SMSView addRedstoneViewToMenu(SMSMenu menu, Location loc, CommandSender owner) throws SMSException { return addRedstoneViewToMenu(null, menu, loc, owner); } /** * Convenience method. Create and register a new SMSSignView object, and attach it to * the given menu. A sign must already exist at the given location, and it must not be * an already-existing view. * * @param menu The menu to attach the new view to * @param loc Location of the new view * @return The newly-created view * @throws SMSException if the given location is not a suitable location for a new view */ public SMSView addSignToMenu(String viewName, SMSMenu menu, Location loc, CommandSender owner) throws SMSException { SMSView view = new SMSSignView(viewName, menu, loc); initialiseView(view, owner); return view; } public SMSView addSignToMenu(SMSMenu menu, Location loc, CommandSender owner) throws SMSException { return addSignToMenu(null, menu, loc, owner); } /** * Convenience method. Create a new spout view and add it to the given menu. * * @param menu the menu to add the view to * @param owner the owner of the view * @return the view that was just created * @throws SMSException */ public SMSView addSpoutViewToMenu(SMSMenu menu, CommandSender owner) throws SMSException { return addSpoutViewToMenu(null, menu, owner); } public SMSView addSpoutViewToMenu(String viewName, SMSMenu menu, CommandSender owner) throws SMSException { SMSView view = new SMSSpoutView(viewName, menu); initialiseView(view, owner); return view; } private UUID getUniqueId(CommandSender owner) { return owner instanceof Player ? ((Player) owner).getUniqueId() : ScrollingMenuSign.CONSOLE_UUID; } /** * Convenience method. Create and register a new SMSPublicHoloView object, and attach it to * the given menu. * * @param viewName Name for the view; if null a name will be auto-generated * @param menu The menu to attach the new view to * @param loc Location of the new view * @return The newly-created view * @throws SMSException if the given location is not a suitable location for the new view */ public SMSView addPublicHoloViewToMenu(String viewName, SMSMenu menu, Location loc, CommandSender owner) { SMSView view = new SMSPublicHoloView(viewName, menu, loc); initialiseView(view, owner); return view; } /** * Convenience method. Create and register a new SMSPrivateHoloView object, and attach it to * the given menu. * * @param viewName Name for the view; if null a name will be auto-generated * @param menu The menu to attach the new view to * @return The newly-created view * @throws SMSException if the given location is not a suitable location for the new view */ public SMSView addPrivateHoloToView(String viewName, SMSMenu menu, CommandSender owner) { SMSView view = new SMSPrivateHoloView(viewName, menu); initialiseView(view, owner); return view; } private void initialiseView(SMSView view, CommandSender owner) { // common setup tasks for a new view registerView(view); view.setAttribute(SMSView.OWNER, view.makeOwnerName(owner)); view.setOwnerId(getUniqueId(owner)); view.update(view.getNativeMenu(), new RepaintAction()); } }