package me.desht.scrollingmenusign; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import me.desht.dhutils.Debugger; import me.desht.dhutils.LogUtils; import me.desht.dhutils.MiscUtil; import me.desht.dhutils.PersistableLocation; import me.desht.dhutils.block.BlockUtil; import me.desht.scrollingmenusign.enums.SMSUserAction; import me.desht.scrollingmenusign.views.SMSGlobalScrollableView; import me.desht.scrollingmenusign.views.SMSView; import org.bukkit.ChatColor; 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.block.Sign; 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; public class RedstoneControlSign implements SMSInteractableBlock { private static final Map<String, Set<Vector>> deferred = new HashMap<String, Set<Vector>>(); private final PersistableLocation location; private final SMSGlobalScrollableView view; private final List<RedstoneControlSign.Action> actions = new ArrayList<RedstoneControlSign.Action>(); private int lastPowerLevel; /** * Construct a new RedstoneControlSign for the given Sign and view. Private constructor - use getControlSign(). * * @param sign the Sign * @param view the globally-scrollable view * @throws SMSException if the sign text is any way invalid */ private RedstoneControlSign(Sign sign, SMSGlobalScrollableView view) { this.location = new PersistableLocation(sign.getLocation()); SMSValidate.isTrue(sign.getLine(0).equals(ChatColor.RED + "[smsred]"), "Sign @ " + MiscUtil.formatLocation(sign.getBlock().getLocation()) + " is not a SMS redstone control sign"); if (view == null) { SMSView baseView = ScrollingMenuSign.getInstance().getViewManager().getView(sign.getLine(1)); SMSValidate.isTrue(baseView instanceof SMSGlobalScrollableView, "view " + sign.getLine(1) + " is not a globally-scrollable view"); this.view = (SMSGlobalScrollableView) baseView; } else { this.view = view; } String line23 = sign.getLine(2) + " " + sign.getLine(3); for (String action : line23.split("\\s+")) { parseAction(action, sign); } lastPowerLevel = sign.getBlock().getBlockPower(); this.view.addControlSign(this); } /** * Get a new RedstoneControlSign for the given location. The block must contain a sign, of which * the first line must read "[smsred]" in red text, and the second line must contain the name of * a globally-scrollable view. * * @param loc The location to check for * @return The RedstoneControlSign at that location. * @throws SMSException if there is no sign at this block or the sign is not valid */ public static RedstoneControlSign getControlSign(Location loc) { return getControlSign(loc, null); } /** * Get a new RedstoneControlSign for the given block and view object. This is called when restoring * RedstoneControlSign from disk. * * @param loc the location to check * @param view the view this is associated with * @return the RedstoneControlSign at this block * @throws SMSException if there is no sign at this block or the sign is not valid */ public static RedstoneControlSign getControlSign(Location loc, SMSGlobalScrollableView view) { Block block = loc.getBlock(); SMSValidate.isTrue(block.getType() == Material.WALL_SIGN || block.getType() == Material.SIGN_POST, "Block @ " + MiscUtil.formatLocation(block.getLocation()) + " does not contain a sign"); LocationManager lm = ScrollingMenuSign.getInstance().getLocationManager(); RedstoneControlSign rcs = lm.getInteractableAt(loc, RedstoneControlSign.class); if (rcs == null) { lm.registerLocation(loc, new RedstoneControlSign((Sign) block.getState(), view)); } return rcs; } /** * Get the last-recorded power level for this control sign. * * @return the last-recorded power level */ public int getLastPowerLevel() { return lastPowerLevel; } /** * Set the last-recorded power level for this control sign. * * @param lastPowerLevel the power level to record */ public void setLastPowerLevel(int lastPowerLevel) { this.lastPowerLevel = lastPowerLevel; } /** * Get the location of this control sign. * * @return the control sign location */ public Location getlocation() { return location.getLocation(); } /** * Get the view that this control sign is attached to. * * @return the view object */ public SMSGlobalScrollableView getView() { return view; } /** * Delete this control sign, detaching it from its view. */ public void delete() { ScrollingMenuSign.getInstance().getLocationManager().unregisterLocation(location.getLocation()); view.removeControlSign(this); } /** * Process the actions for this control sign. Iterate through each defined adjacent block, and when one is * found that is powered, execute the associated action on the control sign's view. Stop processing as soon as * a powered block is found. */ public void processActions() { for (Action a : actions) { Debugger.getInstance().debug("processActions: check " + a.block + " power = " + a.block.isBlockPowered() + "/" + a.block.isBlockIndirectlyPowered()); if (a.block.isBlockPowered() || a.block.isBlockIndirectlyPowered()) { String k = "sms.redstonecontrol." + a.action.toString().toLowerCase(); if (ScrollingMenuSign.getInstance().getConfig().getBoolean(k)) { Debugger.getInstance().debug("processActions: view=" + view.getName() + " action=" + a.action); a.action.execute(null, view); break; } } } } /** * Parse the action definitions in the given string. This would be taken from lines 3 & 4 of * the sign. * * @param action The action string, containing a whitespace-separate list of location/action pairs. */ private void parseAction(String action, Sign sign) { org.bukkit.material.Sign signData = (org.bukkit.material.Sign) sign.getData(); SMSValidate.isTrue(action.length() == 2, "Invalid redstone control spec. '" + action + "' for sign @ " + MiscUtil.formatLocation(sign.getBlock().getLocation())); BlockFace face; switch (Character.toLowerCase(action.charAt(0))) { case 'o': // over face = BlockFace.UP; break; case 'u': // under face = BlockFace.DOWN; break; case 'f': // front face = signData.getFacing(); break; case 'b': // back face = signData.getFacing().getOppositeFace(); break; case 'l': // left face = BlockUtil.getLeft(signData.getFacing().getOppositeFace()); break; case 'r': // right face = BlockUtil.getLeft(signData.getFacing()); break; default: throw new SMSException("Invalid redstone control direction '" + action.charAt(0) + "'"); } SMSUserAction userAction; switch (Character.toLowerCase(action.charAt(1))) { case 'x': userAction = SMSUserAction.EXECUTE; break; case 'u': userAction = SMSUserAction.SCROLLUP; break; case 'd': userAction = SMSUserAction.SCROLLDOWN; break; default: throw new SMSException("Invalid redstone control action '" + action.charAt(1) + "'"); } actions.add(new Action(face, sign.getBlock().getRelative(face), userAction)); } /** * Mark a control sign as deferred - do this if the world isn't loaded at this point. * * @param worldName The name of the world * @param pos a Vector representing the position of the sign */ public static void deferLoading(String worldName, Vector pos) { if (!deferred.containsKey(worldName)) { deferred.put(worldName, new HashSet<Vector>()); } deferred.get(worldName).add(pos); } /** * Load any deferred control signs for the given world. Called from the * WorldLoadEvent handler. * * @param world The world that's been loaded */ public static void loadDeferred(World world) { String worldName = world.getName(); if (!deferred.containsKey(worldName)) { return; } for (Vector pos : deferred.get(world.getName())) { Location loc = new Location(world, pos.getX(), pos.getY(), pos.getZ()); getControlSign(loc); } deferred.remove(world.getName()); } @Override public String toString() { StringBuilder sb = new StringBuilder(MiscUtil.formatLocation(location.getLocation())).append(": "); for (Action a : actions) { sb.append(a.toString()).append(" "); } return sb.toString(); } @Override public void processEvent(ScrollingMenuSign plugin, BlockDamageEvent event) { // ignore } @Override public void processEvent(ScrollingMenuSign plugin, BlockBreakEvent event) { MiscUtil.statusMessage(event.getPlayer(), String.format("Redstone control sign @ &f%s&- was removed from view &e%s&-.", MiscUtil.formatLocation(event.getBlock().getLocation()), getView().getName())); delete(); } @Override public void processEvent(ScrollingMenuSign plugin, BlockPhysicsEvent event) { Block b = event.getBlock(); if (!plugin.getConfigCache().isPhysicsProtected() && BlockUtil.isAttachableDetached(b)) { delete(); LogUtils.info("Redstone control sign for " + getView().getName() + " @ " + location + " has become detached: deleting"); } else { Debugger.getInstance().debug("block physics event @ " + b + " power=" + b.getBlockPower() + " prev-power=" + getLastPowerLevel()); if (b.getBlockPower() > 0 && b.getBlockPower() > getLastPowerLevel()) { processActions(); } setLastPowerLevel(b.getBlockPower()); } } @Override public void processEvent(ScrollingMenuSign plugin, BlockRedstoneEvent event) { Block b = event.getBlock(); Debugger.getInstance().debug("redstone control: " + b + " current=" + event.getNewCurrent() + " power=" + b.getBlockPower()); setLastPowerLevel(b.getBlockPower()); } private class Action { private final BlockFace face; private final Block block; private final SMSUserAction action; Action(BlockFace face, Block block, SMSUserAction action) { this.face = face; this.block = block; this.action = action; Debugger.getInstance().debug("redstone control: create power-on action: " + block + " = " + action); } @Override public String toString() { return face + "=" + action.getShortDesc(); } } }