package me.desht.scrollingmenusign.views;
import me.desht.dhutils.*;
import me.desht.dhutils.block.BlockUtil;
import me.desht.scrollingmenusign.*;
import me.desht.scrollingmenusign.enums.SMSAccessRights;
import me.desht.scrollingmenusign.enums.SMSUserAction;
import me.desht.scrollingmenusign.enums.ViewJustification;
import me.desht.scrollingmenusign.util.Substitutions;
import me.desht.scrollingmenusign.views.action.MenuDeleteAction;
import me.desht.scrollingmenusign.views.action.RepaintAction;
import me.desht.scrollingmenusign.views.action.ViewUpdateAction;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDamageEvent;
import org.bukkit.event.block.BlockPhysicsEvent;
import org.bukkit.event.block.BlockRedstoneEvent;
import org.bukkit.util.Vector;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.*;
/**
* Represents a base menu view from which all concrete views will inherit.
*/
public abstract class SMSView extends CommandTrigger implements Observer, SMSPersistable, ConfigurationListener, SMSInteractableBlock {
// operations which were player-specific (active submenu, scroll position...)
// need to be handled with a single global "player" here...
protected static final String GLOBAL_PSEUDO_PLAYER = "&&global";
// view attribute names
public static final String OWNER = "owner";
public static final String GROUP = "group";
public static final String ITEM_JUSTIFY = "item_justify";
public static final String TITLE_JUSTIFY = "title_justify";
public static final String ACCESS = "access";
private final SMSMenu menu;
private final Set<PersistableLocation> locations = new HashSet<PersistableLocation>();
private final String name;
private final AttributeCollection attributes; // view attributes to be displayed and/or edited by players
private final Map<String, String> variables; // view variables
private final Map<UUID, MenuStack> menuStack; // map player ID to menu stack (submenu support)
private boolean autosave;
private boolean dirty;
private int maxLocations;
private UUID ownerId;
private boolean inThaw;
// we can't use a Set here, since there are three possible values: 1) dirty, 2) clean, 3) unknown
private final Map<UUID, Boolean> dirtyPlayers = new HashMap<UUID, Boolean>();
// map a world name (for a world which hasn't been loaded yet) to a list of x,y,z positions
private final Map<String, List<Vector>> deferredLocations = new HashMap<String, List<Vector>>();
/**
* Get a user-friendly string representing the type of this view.
*
* @return The type of view this is.
*/
public abstract String getType();
public SMSView(SMSMenu menu) {
this(null, menu);
}
public SMSView(String name, SMSMenu menu) {
if (name == null) {
name = makeUniqueName(menu.getName());
}
this.name = name;
this.menu = menu;
this.dirty = true;
this.autosave = true;
this.attributes = new AttributeCollection(this);
this.variables = new HashMap<String, String>();
this.maxLocations = 1;
this.menuStack = new HashMap<UUID, MenuStack>();
attributes.registerAttribute(OWNER, ScrollingMenuSign.CONSOLE_OWNER, "Player who owns this view");
attributes.registerAttribute(GROUP, "", "Permission group for this view");
attributes.registerAttribute(TITLE_JUSTIFY, ViewJustification.DEFAULT, "Horizontal title positioning");
attributes.registerAttribute(ITEM_JUSTIFY, ViewJustification.DEFAULT, "Horizontal item positioning");
attributes.registerAttribute(ACCESS, SMSAccessRights.ANY, "Who may use this view");
}
private String makeUniqueName(String base) {
int idx = 1;
String s = String.format("%s-%d", base, idx);
while (ScrollingMenuSign.getInstance().getViewManager().checkForView(s)) {
idx++;
s = String.format("%s-%d", base, idx);
}
return s;
}
/**
* Get the view's autosave status - will the view be automatically saved to disk when modified?
*
* @return true if the view will be autosaved, false otherwise
*/
public boolean isAutosave() {
return autosave;
}
/**
* Set the view's autosave status - will the view be automatically saved to disk when modified?
*
* @param autosave true if the view will be autosaved, false otherwise
*/
public void setAutosave(boolean autosave) {
this.autosave = autosave;
}
/* (non-Javadoc)
* @see java.util.Observer#update(java.util.Observable, java.lang.Object)
*/
@Override
public void update(Observable o, Object arg1) {
if (o == null) {
return;
}
SMSMenu m = (SMSMenu) o;
ViewUpdateAction vu = ViewUpdateAction.getAction(arg1);
Debugger.getInstance().debug("update: view=" + getName() + " action=" + vu.getClass().getSimpleName()
+ " player=" + vu.getSender() + " menu=" + m.getName() + ", nativemenu=" + getNativeMenu().getName());
if (m == getNativeMenu()) {
if (vu instanceof MenuDeleteAction) {
ScrollingMenuSign.getInstance().getViewManager().deleteView(this, ((MenuDeleteAction) vu).isPermanent());
}
}
}
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.Freezable#getName()
*/
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.views.CommandTrigger#getName()
*/
@Override
public String getName() {
return name;
}
/**
* Get the native menu associated with the view. The native menu is the menu that
* the view was originally created for.
*
* @return The native SMSMenu object for this view.
*/
public SMSMenu getNativeMenu() {
return menu;
}
public UUID getOwnerId() {
return ownerId;
}
public void setOwnerId(UUID ownerId) {
this.ownerId = ownerId;
if (ownerId == null || ownerId.equals(ScrollingMenuSign.CONSOLE_UUID)) {
setAttribute(OWNER, ScrollingMenuSign.CONSOLE_OWNER);
} else {
String name = Bukkit.getOfflinePlayer(ownerId).getName();
setAttribute(OWNER, name == null ? "???" : name);
}
autosave();
}
/**
* Get the currently active menu for this view for the given player. This is not necessarily the
* same as the view's native menu, if the player has a submenu open in this view.
*
* @return the active SMSMenu object for this view
*/
public SMSMenu getActiveMenu(Player player) {
UUID key = getPlayerContext(player);
if (!menuStack.containsKey(key)) {
menuStack.put(key, new MenuStack());
}
MenuStack mst = menuStack.get(key);
return mst.isEmpty() ? getNativeMenu() : mst.peek();
}
/**
* Push the given menu onto the view, making it the active menu as returned by {@link #getActiveMenu(Player)}
*
* @param player player to push the menu for
* @param newActive the menu to make active
*/
public void pushMenu(Player player, SMSMenu newActive) {
UUID key = getPlayerContext(player);
getActiveMenu(player).deleteObserver(this);
if (!menuStack.containsKey(key)) {
menuStack.put(key, new MenuStack());
}
menuStack.get(key).pushMenu(newActive);
newActive.addObserver(this);
Debugger.getInstance().debug("pushed menu " + newActive.getName() + " onto " + getName()
+ " player=" + player.getName() + " player-context = " + key);
update(newActive, new RepaintAction());
}
/**
* Pop the active menu off the view, making the previously active menu the new active menu.
*
* @param player player to pop the menu for
* @return the active menu that has just been popped off
*/
public SMSMenu popMenu(Player player) {
UUID key = getPlayerContext(player);
if (!menuStack.containsKey(key)) {
menuStack.put(key, new MenuStack());
}
MenuStack mst = menuStack.get(key);
SMSMenu oldActive = mst.popMenu();
if (oldActive == null) {
return null;
}
oldActive.deleteObserver(this);
SMSMenu newActive = getActiveMenu(player);
newActive.addObserver(this);
Debugger.getInstance().debug("popped menu " + oldActive.getName() + " off " + getName() + " new active = " + newActive.getName()
+ " player=" + player.getName() + " player-context = " + key);
update(newActive, new RepaintAction());
return oldActive;
}
/**
* Get the set of player IDs who have a submenu open for this view.
*
* @return a set of player IDs who have a submenu open for this view
*/
public Set<UUID> getSubmenuPlayers() {
return menuStack.keySet();
}
MenuStack getMenuStack(UUID playerId) {
return menuStack.get(playerId);
}
@Override
public String getActiveItemLabel(Player player, int pos) {
String label = super.getActiveItemLabel(player, pos);
return label == null ? null : doVariableSubstitutions(player, label);
}
/**
* Set an arbitrary string of tagged data on this view.
*
* @param key the variable name (must contain only alphanumeric or underscore)
* @param val the variable value (may contain any character)
*/
public void setVariable(String key, String val) {
SMSValidate.isTrue(key.matches("[A-Za-z0-9_]+"), "Invalid variable name: " + key);
if (val == null) {
variables.remove(key);
} else {
variables.put(key, val);
}
}
/**
* Get an arbitrary string of tagged data from this view
*
* @param key the variable name (must contain only alphanumeric or underscore)
* @return the variable value
*/
public String getVariable(String key) {
SMSValidate.isTrue(key.matches("[A-Za-z0-9_]+"), "Invalid variable name: " + key);
SMSValidate.isTrue(variables.containsKey(key), "View " + getName() + " has no variable: " + key);
return variables.get(key);
}
/**
* Check if the given view variable exists in this view.
*
* @param key the variable name (must contain only alphanumeric or underscore)
* @return true if the variable exists, false otherwise
*/
public boolean checkVariable(String key) {
return variables.containsKey(key);
}
/**
* Get a list of all variable names for this view.
*
* @return a list of variable names for this view
*/
public Set<String> listVariables() {
return variables.keySet();
}
/**
* Get the justification for menu items in this view
*
* @return the justification for menu items in this view
*/
public ViewJustification getItemJustification() {
return getJustification("sms.item_justify", ITEM_JUSTIFY, ViewJustification.LEFT);
}
/**
* Get the justification for the menu title in this view
*
* @return the justification for the menu title in this view
*/
public ViewJustification getTitleJustification() {
return getJustification("sms.title_justify", TITLE_JUSTIFY, ViewJustification.CENTER);
}
private ViewJustification getJustification(String configItem, String attrName, ViewJustification fallback) {
ViewJustification viewJust = (ViewJustification) getAttribute(attrName);
if (viewJust != ViewJustification.DEFAULT) {
return viewJust;
}
String j = ScrollingMenuSign.getInstance().getConfig().getString(configItem, fallback.toString());
try {
return ViewJustification.valueOf(j.toUpperCase());
} catch (IllegalArgumentException e) {
return fallback;
}
}
public String doVariableSubstitutions(Player player, String text) {
String s = Substitutions.viewVariableSubs(this, text);
s = Substitutions.userVariableSubs(player, s, null);
return s;
}
public List<String> doVariableSubstitutions(Player player, List<String> l) {
List<String> res = new ArrayList<String>(l.size());
for (String s : l) {
res.add(doVariableSubstitutions(player, s));
}
return res;
}
public Map<String, Object> freeze() {
Map<String, Object> map = new HashMap<String, Object>();
Map<String, String> vars = new HashMap<String, String>();
map.put("name", name);
map.put("menu", menu.getName());
map.put("class", getClass().getName());
map.put("owner_id", getOwnerId() == null ? "" : getOwnerId().toString());
for (String key : listAttributeKeys(false)) {
map.put(key, attributes.get(key).toString());
}
List<PersistableLocation> locs = new ArrayList<PersistableLocation>();
for (Location l : getLocations()) {
PersistableLocation pl = new PersistableLocation(l);
pl.setSavePitchAndYaw(false);
locs.add(pl);
}
map.put("locations", locs);
for (String key : listVariables()) {
vars.put(key, getVariable(key));
}
map.put("vars", vars);
return map;
}
@SuppressWarnings("unchecked")
protected void thaw(ConfigurationSection node) throws SMSException {
inThaw = true;
List<Object> locs = (List<Object>) node.getList("locations");
for (Object o : locs) {
if (o instanceof PersistableLocation) {
PersistableLocation pl = (PersistableLocation) o;
try {
addLocation(pl.getLocation());
} catch (IllegalStateException e) {
// world not loaded? we'll defer adding this location to the view for now
// perhaps the world will get loaded later
addDeferredLocation(pl.getWorldName(), new Vector(pl.getX(), pl.getY(), pl.getZ()));
}
} else {
throw new SMSException("invalid location in view " + getName() + " (corrupted file?)");
}
}
String id = node.getString("owner_id");
if (id != null && !id.isEmpty()) {
setOwnerId(UUID.fromString(id));
}
// temporarily disable validation while attributes are loaded from saved data
attributes.setValidate(false);
for (String key : node.getKeys(false)) {
if (!node.isConfigurationSection(key) && attributes.hasAttribute(key)) {
String val = node.getString(key);
try {
setAttribute(key, val);
} catch (SMSException e) {
LogUtils.warning("View " + getName() + ": can't set " + key + "='" + val + "': " + e.getMessage());
}
}
}
// ensure view has an owner (pre-2.0, views did not)
String owner = getAttributeAsString(OWNER);
if (owner.isEmpty()) {
setAttribute(OWNER, getNativeMenu().getOwner());
}
ConfigurationSection vars = node.getConfigurationSection("vars");
if (vars != null) {
for (String k : vars.getKeys(false)) {
setVariable(k, vars.getString(k));
}
}
attributes.setValidate(true);
inThaw = false;
}
/**
* Mark a location (actually a world name and a x,y,z vector) as deferred - the world isn't
* currently available.
*
* @param worldName name of the world
* @param v a vector describing the location
*/
private void addDeferredLocation(String worldName, Vector v) {
List<Vector> l = deferredLocations.get(worldName);
if (l == null) {
l = new ArrayList<Vector>();
deferredLocations.put(worldName, l);
}
l.add(v);
}
/**
* Get a list of the locations (x,y,z vectors) that have been deferred for the given world name.
*
* @param worldName the name of the world to check for
* @return a list of vectors; the locations that have been deferred
*/
public List<Vector> getDeferredLocations(String worldName) {
return deferredLocations.get(worldName);
}
/**
* Get the "dirty" status for this view - whether or not a repaint is needed for all players.
*
* @return true if a repaint is needed, false otherwise
*/
public boolean isDirty() {
return dirty;
}
/**
* Get the "dirty" status for this view - whether or not a repaint is needed for the given player.
*
* @param player The player to check for
* @return true if a repaint is needed, false otherwise
*/
public boolean isDirty(Player player) {
UUID key = getPlayerContext(player);
return dirtyPlayers.containsKey(key) ? dirtyPlayers.get(key) : dirty;
}
/**
* Set the global "dirty" status for this view - whether or not a repaint is needed for all players.
*
* @param dirty true if a repaint is needed, false otherwise
*/
public void setDirty(boolean dirty) {
setDirty(null, dirty);
}
/**
* Set the per-player "dirty" status for this view - whether or not a repaint is needed for the given player.
* If a null player is passed, the view is marked as dirty for all players.
*
* @param player The player to check for, may be null
* @param dirty Whether or not a repaint is needed
*/
public void setDirty(Player player, boolean dirty) {
if (player == null) {
this.dirty = dirty;
if (dirty) {
dirtyPlayers.clear();
}
} else {
dirtyPlayers.put(getPlayerContext(player), dirty);
}
}
/**
* Get a set of all locations for this view. Views may have zero or more locations (e.g. a sign
* view has one location, a map view has zero locations, a multisign view has several locations...)
*
* @return A Set of all locations for this view object
* @throws IllegalStateException if the world for this view has become unloaded
*/
public Set<Location> getLocations() {
Set<Location> res = new HashSet<Location>();
for (PersistableLocation l : locations) {
res.add(l.getLocation());
}
return res;
}
/**
* Get a list of all locations for this view as a Java array.
*
* @return An array of all locations for this view object
*/
public Location[] getLocationsArray() {
Set<Location> locs = getLocations();
return locs.toArray(new Location[locs.size()]);
}
/**
* Set the maximum number of locations which are allowed. Subclass constructors should call this
* as appropriate.
*
* @param maxLocations the maximum number of locations to be allowed
*/
protected void setMaxLocations(int maxLocations) {
this.maxLocations = maxLocations;
}
/**
* Get the maximum number of locations that this view may occupy.
*
* @return The maximum number of locations
*/
public int getMaxLocations() {
return maxLocations;
}
/**
* Build the name of the player (or possibly the console) which owns this view.
*
* @param sender the command sender object
* @return the name of the command sender
*/
String makeOwnerName(CommandSender sender) {
return sender != null && sender instanceof Player ? sender.getName() : ScrollingMenuSign.CONSOLE_OWNER;
}
/**
* Register a new location as being part of this view object
*
* @param loc The location to register
* @throws SMSException if the location is not suitable for adding to this view
*/
public void addLocation(Location loc) throws SMSException {
SMSValidate.isTrue(getLocations().size() < getMaxLocations(),
"View " + getName() + " already occupies the maximum number of locations (" + getMaxLocations() + ")");
ViewManager viewManager = ScrollingMenuSign.getInstance().getViewManager();
SMSView v = viewManager.getViewForLocation(loc);
if (v != null) {
throw new SMSException("Location " + MiscUtil.formatLocation(loc) + " already contains a view on menu: " + v.getNativeMenu().getName());
}
locations.add(new PersistableLocation(loc));
if (viewManager.checkForView(getName())) {
viewManager.registerLocation(loc, this);
}
autosave();
}
/**
* Unregister a location from the given view.
*
* @param loc The location to unregister.
*/
public void removeLocation(Location loc) {
ViewManager viewManager = ScrollingMenuSign.getInstance().getViewManager();
locations.remove(new PersistableLocation(loc));
viewManager.unregisterLocation(loc);
autosave();
}
/**
* Save this view's contents to disk (if autosaving is enabled, and the view
* is registered).
*/
public void autosave() {
if (isAutosave() && ScrollingMenuSign.getInstance().getViewManager().checkForView(getName()))
SMSPersistence.save(this);
}
/* (non-Javadoc)
* @see me.desht.scrollingmenusign.SMSPersistable#getSaveFolder()
*/
public File getSaveFolder() {
return DirectoryStructure.getViewsFolder();
}
/**
* Check if the given player has access rights for this view.
*
* @param player The player to check
* @return True if the player may use this view, false if not
*/
public boolean hasOwnerPermission(Player player) {
if (!getActiveMenu(player).hasOwnerPermission(player)) {
return false;
}
SMSAccessRights access = (SMSAccessRights) getAttribute(ACCESS);
return access.isAllowedToUse(player, ownerId, getAttributeAsString(OWNER), getAttributeAsString(GROUP));
}
/**
* Check if this view is owned by the given player.
*
* @param player the player to check
* @return true if the view is owned by the player, false otherwise
*/
public boolean isOwnedBy(Player player) {
return player.getUniqueId().equals(ownerId);
}
/**
* Require that the given command sender is allowed to use this view, and throw a SMSException if not.
*
* @param sender the command sender to check
* @throws SMSException if the command sender is not allowed to use this view
*/
public void ensureAllowedToUse(CommandSender sender) {
if (sender instanceof Player) {
Player player = (Player) sender;
if (!hasOwnerPermission(player)) {
throw new SMSException("That menu view is private to someone else.");
}
if (!isTypeUsable(player)) {
throw new SMSException("You don't have permission to use " + getType() + " menu views.");
}
}
}
protected boolean isTypeUsable(Player player) {
return PermissionUtils.isAllowedTo(player, "scrollingmenusign.use." + getType());
}
/**
* Require that the given command sender is allowed to modify this view, and throw a SMSException if not.
*
* @param sender the command sender to check
* @throws SMSException if the command sender is not allowed to modify this view
*/
public void ensureAllowedToModify(CommandSender sender) {
if (sender instanceof Player) {
Player player = (Player) sender;
if (!PermissionUtils.isAllowedTo(player, "scrollingmenusign.edit.any") && !isOwnedBy(player)) {
throw new SMSException("That view is owned by someone else.");
}
}
}
protected void registerAttribute(String attr, Object def, String desc) {
attributes.registerAttribute(attr, def, desc);
}
protected void registerAttribute(String attr, Object def) {
attributes.registerAttribute(attr, def);
}
public AttributeCollection getAttributes() {
return attributes;
}
public Object getAttribute(String k) {
return attributes.get(k);
}
public String getAttributeAsString(String k, String def) {
Object o = getAttribute(k);
return o == null || o.toString().isEmpty() ? def : o.toString();
}
public String getAttributeAsString(String k) {
return getAttributeAsString(k, "");
}
public void setAttribute(String k, String val) throws SMSException {
SMSValidate.isTrue(attributes.contains(k), "No such view attribute: " + k);
attributes.set(k, val);
}
public Set<String> listAttributeKeys(boolean isSorted) {
return attributes.listAttributeKeys(isSorted);
}
/* (non-Javadoc)
* @see me.desht.dhutils.ConfigurationListener#onConfigurationChanged(me.desht.dhutils.ConfigurationManager, java.lang.String, java.lang.Object, java.lang.Object)
*/
@Override
public void onConfigurationChanged(ConfigurationManager configurationManager, String key, Object oldVal, Object newVal) {
if (key.equals(OWNER) && !inThaw) {
// try to get the owner UUID matching the owner name if possible
final String owner = newVal.toString();
if (owner.isEmpty() || owner.equals(ScrollingMenuSign.CONSOLE_OWNER)) {
ownerId = ScrollingMenuSign.CONSOLE_UUID;
} else if (MiscUtil.looksLikeUUID(owner)) {
ownerId = UUID.fromString(owner);
String name = Bukkit.getOfflinePlayer(ownerId).getName();
setAttribute(OWNER, name == null ? "?" : name);
} else if (!owner.equals("?")) {
@SuppressWarnings("deprecation") Player p = Bukkit.getPlayer(owner);
if (p != null) {
ownerId = p.getUniqueId();
} else {
updateOwnerAsync(owner);
}
}
}
// Don't do updates on views that haven't been registered yet (which will be the case
// when restoring saved views from disk)
if (ScrollingMenuSign.getInstance().getViewManager().checkForView(getName())) {
update(null, new RepaintAction());
}
}
private void updateOwnerAsync(final String owner) {
final UUIDFetcher uf = new UUIDFetcher(Arrays.asList(owner));
Bukkit.getScheduler().runTaskAsynchronously(ScrollingMenuSign.getInstance(), new Runnable() {
@Override
public void run() {
try {
Map<String,UUID> res = uf.call();
if (res.containsKey(owner)) {
ownerId = res.get(owner);
} else {
LogUtils.warning("View [" + getName() + "]: no known UUID for player: " + owner);
ownerId = ScrollingMenuSign.CONSOLE_UUID;
}
} catch (Exception e) {
LogUtils.warning("View [" + getName() + "]: can't retrieve UUID for player: " + owner + ": " + e.getMessage());
}
}
});
}
/* (non-Javadoc)
* @see me.desht.dhutils.ConfigurationListener#onConfigurationValidate(me.desht.dhutils.ConfigurationManager, java.lang.String, java.lang.String)
*/
@Override
public Object onConfigurationValidate(ConfigurationManager configurationManager, String key, Object oldVal, Object newVal) {
if (key.equals(ACCESS)) {
SMSAccessRights access = (SMSAccessRights) newVal;
if (access != SMSAccessRights.ANY && ownerId == null) {
throw new SMSException("View must be owned by a player to change access control to " + access);
} else if (access == SMSAccessRights.GROUP && ScrollingMenuSign.permission == null) {
throw new SMSException("Cannot use GROUP access control (no permission group support available)");
}
}
return newVal;
}
/**
* Erase the view's contents and perform any housekeeping; called when it's about to be deleted.
*/
public void onDeleted(boolean permanent) {
// does nothing by default: override in subclasses
}
/**
* Called automatically when the view is used to execute a menu item. Override and extend this
* in subclasses.
*
* @param player The player who did the execution
*/
public void onExecuted(Player player) {
// does nothing by default: override in subclasses
}
/**
* Called automatically when the view is scrolled. Override and extend this
* in subclasses.
*
* @param player The player who did the scrolling
* @param action The scroll direction: SCROLLDOWN or SCROLLUP
*/
public void onScrolled(Player player, SMSUserAction action) {
// does nothing by default: override in subclasses
}
/**
* Called automatically when a player logs out. Perform any cleardown work to remove player
* records from the view. Override and extend this in subclasses.
*
* @param player The player who logged out
*/
public void clearPlayerForView(Player player) {
// does nothing by default: override in subclasses
}
public void processEvent(ScrollingMenuSign plugin, BlockDamageEvent event) {
Block b = event.getBlock();
Player player = event.getPlayer();
SMSMenu menu = getNativeMenu();
if (Debugger.getInstance().getLevel() > 0) {
Debugger.getInstance().debug("block damage event @ " + MiscUtil.formatLocation(b.getLocation()) + ", view = " + getName() + ", menu=" + menu.getName());
}
if (plugin.getConfigCache().isBreakProtected() ||
!menu.isOwnedBy(player) && !PermissionUtils.isAllowedTo(player, "scrollingmenusign.edit.any")) {
event.setCancelled(true);
}
}
public void processEvent(ScrollingMenuSign plugin, BlockBreakEvent event) {
Player player = event.getPlayer();
Block b = event.getBlock();
if (Debugger.getInstance().getLevel() > 0) {
Debugger.getInstance().debug("block break event @ " + b.getLocation() + ", view = " + getName() + ", menu=" + getNativeMenu().getName());
}
if (plugin.getConfigCache().isBreakProtected()) {
event.setCancelled(true);
update(getActiveMenu(player), new RepaintAction());
} else {
removeLocation(b.getLocation());
if (getLocations().isEmpty()) {
plugin.getViewManager().deleteView(this, true);
}
MiscUtil.statusMessage(player,
String.format("%s block @ &f%s&- was removed from view &e%s&- (menu &e%s&-).",
b.getType(), MiscUtil.formatLocation(b.getLocation()), getName(), getNativeMenu().getName()));
}
}
public void processEvent(ScrollingMenuSign plugin, BlockPhysicsEvent event) {
Block b = event.getBlock();
if (Debugger.getInstance().getLevel() > 0) {
Debugger.getInstance().debug("block physics event @ " + b.getLocation() + ", view = " + getName() + ", menu=" + getNativeMenu().getName());
}
if (plugin.getConfigCache().isPhysicsProtected()) {
event.setCancelled(true);
} else if (BlockUtil.isAttachableDetached(b)) {
// attached to air? looks like the sign (or other attachable) has become detached
// NOTE: for multi-block views, the loss of *any* block due to physics causes the view to be removed
LogUtils.info("Attachable view block " + getName() + " @ " + b.getLocation() + " has become detached: deleting");
plugin.getViewManager().deleteView(this, true);
}
}
public void processEvent(ScrollingMenuSign plugin, BlockRedstoneEvent event) {
Block b = event.getBlock();
if (Debugger.getInstance().getLevel() > 0) {
Debugger.getInstance().debug("block redstone event @ " + b.getLocation() + ", view = "
+ getName() + ", menu = " + getNativeMenu().getName()
+ ", current = " + event.getOldCurrent() + "->" + event.getNewCurrent());
}
}
/**
* Get a view by name. Backwards-compatibility for other plugins which need it.
*
* @param viewName name of the view to get
* @return the view object
* @throws SMSException if there is no such view
* @deprecated use ViewManager#getView(String)
*/
@Deprecated
public static SMSView getView(String viewName) {
return ScrollingMenuSign.getInstance().getViewManager().getView(viewName);
}
/**
* Register a view with the view manager. Backwards-compatibility for other plugins which need it.
*
* @deprecated use ViewManager#registerView(SMSView)
*/
@Deprecated
public void register() {
ScrollingMenuSign.getInstance().getViewManager().registerView(this);
}
/**
* Check if this view responds to player clicks to scroll/execute it.
*
* @return true if the view is responsive to clicks, false otherwise
*/
public boolean isClickable() {
return true;
}
/**
* Represents a stack of menus, for submenu support. The currently-active
* menu is at the top of the stack.
*/
public class MenuStack {
final Deque<WeakReference<SMSMenu>> stack;
public MenuStack() {
stack = new ArrayDeque<WeakReference<SMSMenu>>();
}
public void pushMenu(SMSMenu menu) {
stack.push(new WeakReference<SMSMenu>(menu));
}
public SMSMenu popMenu() {
return stack.pop().get();
}
public SMSMenu peek() {
return stack.peek().get();
}
public boolean isEmpty() {
return stack.isEmpty();
}
}
}