src/main/java/io/github/redwallhp/athenagm/modules/kits/KitsModule.java
public class KitsModule implements Module {


    private AthenaGM plugin;


    public String getModuleName() {
        return "kits";
    }


    public KitsModule(AthenaGM plugin) {
        this.plugin = plugin;
    }


    public void unload() {}


    /**
     * When a player respawns, give them the appropriate kit from the map configuration
     */
    @EventHandler
    public void giveKitOnRespawn(PlayerMatchRespawnEvent event) {

        Player player = event.getPlayer();
        Match match = plugin.getArenaHandler().getArenaForPlayer(player).getMatch();
        Team playerTeam = PlayerUtil.getTeamForPlayer(match, player);
        int roll = (int) (Math.random() * 100);

        if (playerTeam == null || playerTeam.isSpectator()) return; //skip spectators and nonexistant teams

        if (playerTeam.getKitItems() != null) {
            Inventory inventory = player.getInventory();
            for (MapInfoKitItem item : playerTeam.getKitItems()) {
                if (item.getInventorySlot() < 41 && item.getInventorySlot() >= 0) {
                    if (roll > item.getChance()) continue; //handle random chance items
                    inventory.setItem(item.getInventorySlot(), item.getItem());
                }
            }
            player.updateInventory();
        }

    }


    /**
     * Blacklist item drops flagged as "drop: false" in the kit definition
     */
    @EventHandler
    public void onPlayerMatchDeathEvent(AthenaDeathEvent event) {

        if (event.getPlayerTeam().getKitItems() == null) return;

        Set<ItemStack> blacklist = new HashSet<>();
        for (MapInfoKitItem kitItem : event.getPlayerTeam().getKitItems()) {
            if (!kitItem.dropOnDeath()) blacklist.add(kitItem.getItem());
        }

        ListIterator<ItemStack> it = event.getDrops().listIterator();
        while (it.hasNext()) {
            ItemStack item = it.next();
            for (ItemStack blacklisted : blacklist) {
                if (blacklisted.getType().equals(item.getType())) {
                    it.remove();
                    break; //only remove one item per pass, to prevent IllegalStateException
                }
            }
        }

    }


}
src/main/java/io/github/redwallhp/athenagm/modules/Permissions/PermissionsModule.java
public class PermissionsModule implements Module {


    private AthenaGM plugin;
    private HashMap<UUID, PermissionAttachment> attachments;
    private File groupsFile;
    private File usersFile;
    private FileConfiguration groupsConfig;
    private FileConfiguration usersConfig;
    private HashMap<String, Group> groups;
    private HashMap<UUID, User> users;


    public String getModuleName() {
        return "permissions";
    }


    public PermissionsModule(AthenaGM plugin) {
        this.plugin = plugin;
        this.attachments = new HashMap<UUID, PermissionAttachment>();
        this.groupsFile = new File(plugin.getDataFolder(), "groups.yml");
        this.usersFile = new File(plugin.getDataFolder(), "users.yml");
        copyDefaults();
        loadPermissions();
    }


    public void unload() {
        removeAllAttachments();
    }


    /**
     * When a player joins, apply a PermissionAttachment to the player so this
     * module can manage permissions for them.
     */
    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        addAttachment(event.getPlayer());
        applyPermissions(event.getPlayer());
    }


    /**
     * Remove the PermissionAttachment when the player leaves
     */
    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent event) {
        removeAttachment(event.getPlayer());
    }


    /**
     * Remove the PermissionAttachment when the player leaves
     */
    @EventHandler
    public void onPlayerKick(PlayerKickEvent event) {
        removeAttachment(event.getPlayer());
    }


    /**
     * Method to add a PermissionAttachment to a player, so the module can manage
     * their permissions. Saves a reference so permissions can be manipulated later.
     * @param player
     */
    public void addAttachment(Player player) {
        PermissionAttachment attachment = player.addAttachment(plugin);
        this.attachments.put(player.getUniqueId(), attachment);
    }


    /**
     * Method to remove a PermissionAttachment from a player
     */
    public void removeAttachment(Player player) {
        if (attachments.containsKey(player.getUniqueId())) {
            player.removeAttachment(attachments.get(player.getUniqueId()));
            this.attachments.remove(player.getUniqueId());
        }
    }


    /**
     * Method to remove all PermissionAttachments from players.
     * Called on module unload.
     */
    public void removeAllAttachments() {
        for (Player player : plugin.getServer().getOnlinePlayers()) {
            removeAttachment(player);
        }
    }


    /**
     * Move the default configuration files from the jar to the plugin's data directory.
     * groups.yml sets up groups and the permissions they have.
     * users.yml is for user-group membership.
     */
    private void copyDefaults() {
        if (!groupsFile.exists()) {
            plugin.saveResource("groups.yml", false);
        }
        if (!usersFile.exists()) {
            plugin.saveResource("users.yml", false);
        }
    }


    /**
     * Parse the YAML files and load the permission settings from them.
     */
    private void loadPermissions() {

        groupsConfig = YamlConfiguration.loadConfiguration(groupsFile);
        usersConfig = YamlConfiguration.loadConfiguration(usersFile);

        // Load groups
        this.groups = new HashMap<String, Group>();
        Set<String> groupNames = groupsConfig.getConfigurationSection("groups").getKeys(false);
        for (String name : groupNames) {
            List<String> inherits = groupsConfig.getStringList(String.format("groups.%s.inherits", name));
            List<String> nodes = groupsConfig.getStringList(String.format("groups.%s.permissions", name));
            List<Permission> permissions = new ArrayList<Permission>();
            for (String node : nodes) {
                permissions.add(new Permission(node));
            }
            Group group = new Group(name, inherits, permissions);
            this.groups.put(name, group);
        }

        // Load users
        this.users = new HashMap<UUID, User>();
        Set<String> userKeys = usersConfig.getConfigurationSection("users").getKeys(false);
        for (String key : userKeys) {
            UUID uuid = UUID.fromString(key);
            String group = usersConfig.getString(String.format("users.%s.group", key));
            User user = new User(uuid, group);
            this.users.put(uuid, user);
        }

    }


    /**
     * Reload permissions by re-parsing the files, removing existing permissions from
     * every online player, and re-adding the new ones.
     */
    public void reloadPermissions() {
        loadPermissions();
        for (Player player : plugin.getServer().getOnlinePlayers()) {
            removePermissions(player);
            applyPermissions(player);
        }
    }


    /**
     * Remove all permissions from the specified player.
     */
    private void removePermissions(Player player) {
        PermissionAttachment attachment = attachments.get(player.getUniqueId());
        for (String perm : attachment.getPermissions().keySet()) {
            attachment.unsetPermission(perm);
        }
    }


    /**
     * Look up the permissions for the specified player and apply them
     */
    private void applyPermissions(Player player) {
        PermissionAttachment attachment = attachments.get(player.getUniqueId());
        User user = null;
        if (users.containsKey(player.getUniqueId())) {
            user = users.get(player.getUniqueId());
        } else {
            user = new User(player.getUniqueId(), "default");
        }
        if (!this.groups.containsKey(user.getGroup())) {
            user.setGroup("default");
        }
        Group group = this.groups.get(user.getGroup());
        List<Permission> permissions = getGroupPermissions(group);
        for (Permission perm : permissions) {
            attachment.setPermission(perm.getNode(), perm.isPermit());
        }
    }


    /**
     * Get the complete list of permission nodes for a group by
     * iterating all of the inherited groups and consolidating the list.
     */
    private List<Permission> getGroupPermissions(Group group) {
        List<Permission> permissions = new ArrayList<Permission>();
        List<Group> groups = new ArrayList<Group>();
        groups.add(group);
        if (!group.getName().equals("default") && this.groups.containsKey("default")) {
            groups.add(this.groups.get("default"));
        }
        for (String name : group.getInherits()) {
            if (this.groups.containsKey(name)) {
                groups.add(this.groups.get(name));
            }
        }
        for (Group g : groups) {
            for (Permission perm : g.getPermissions()) {
                if (!permissions.contains(perm)) {
                    permissions.add(perm);
                }
            }
        }
        return permissions;
    }


    /**
     * Get the group map
     */
    public HashMap<String, Group> getGroups() {
        return groups;
    }


    /**
     * Get the user map
     */
    public HashMap<UUID, User> getUsers() {
        return users;
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/scoreboard/ScoreboardModule.java
public class ScoreboardModule implements Module {


    private AthenaGM plugin;
    private HashMap<Match, Scoreboard> boards;


    public String getModuleName() {
        return "scoreboard";
    }


    public ScoreboardModule(AthenaGM plugin) {
        this.plugin = plugin;
        this.boards = new HashMap<Match, Scoreboard>();
    }


    public void unload() {}


    /**
     * When a new match is created, prepare the scoreboard for this match
     * and ensure old ones are destroyed.
     */
    @EventHandler
    public void onMatchCreate(MatchCreateEvent event) {
        clearUnusedScoreboards();
        registerMatchScoreboardAndTeams(event.getMatch());
        createSidebarTeamScoreObjective(event.getMatch());
        createPlayerHealthObjective(event.getMatch());
    }


    /**
     * When a player changes team, make sure the scoreboard reflects it
     */
    @EventHandler
    public void onPlayerChangedTeam(PlayerChangedTeamEvent event) {
        Scoreboard board = boards.get(event.getTeam().getMatch());
        org.bukkit.scoreboard.Team sbTeam = board.getTeam(event.getTeam().getId());
        sbTeam.addPlayer(event.getPlayer());
    }


    /**
     * When a player enters a match world, affix the corresponding scoreboard
     */
    @EventHandler
    public void onPlayerEnterMatchWorld(PlayerEnterMatchWorldEvent event) {
        event.getPlayer().setScoreboard(boards.get(event.getMatch()));
    }


    /**
     * Update sidebar score box when points are scored
     */
    @EventHandler
    public void onPlayerScorePointEvent(PlayerScorePointEvent event) {
        Objective objective = boards.get(event.getTeam().getMatch()).getObjective("matchscore");
        Score score = objective.getScore(event.getTeam().getChatColor() + event.getTeam().getName());
        score.setScore(score.getScore() + event.getPointsScored());
    }


    @EventHandler
    public void onMatchTimerTickEvent(MatchTimerTickEvent event) {
        updateMatchTimeDisplay(event.getMatchTimer().getMatch(), event.getMatchTimer().timeLeftInSeconds());
        syncScores(event.getMatchTimer().getMatch());
    }


    /**
     * Update the scoreboard once every second, to account for team.incrementPoints()
     */
    private void syncScores(Match match) {
        Score score;
        Objective objective = boards.get(match).getObjective("matchscore");
        for (Team team : match.getTeams().values()) {
            if (!team.isSpectator()) {
                score = objective.getScore(team.getChatColor() + team.getName());
                score.setScore(team.getPoints());
            }
        }
    }


    /**
     * Register the scoreboard and set up the internal teams based on
     * the Match's Team objects.
     */
    private void registerMatchScoreboardAndTeams(Match match) {

        // Create scoreboard
        Scoreboard board = Bukkit.getScoreboardManager().getNewScoreboard();
        this.boards.put(match, board);

        // Create teams for the scoreboard
        for (Team team : match.getTeams().values()) {
            org.bukkit.scoreboard.Team sbTeam = board.registerNewTeam(team.getId());
            sbTeam.setDisplayName(team.getColoredName());
            sbTeam.setPrefix(team.getChatColor() + "");
        }

        // Add online players to the appropriate scoreboard team objects
        // This will colorize their names in nameplates and the tab list
        for (Team team : match.getTeams().values()) {
            org.bukkit.scoreboard.Team sbTeam = board.getTeam(team.getId());
            for (Player player : team.getPlayers()) {
                sbTeam.addPlayer(player);
                player.setScoreboard(board);
            }
        }

    }


    /**
     * Remove scoreboards from ended Matches
     */
    private void clearUnusedScoreboards() {
        Set<Match> activeMatches = new HashSet<>();
        for (Arena arena : plugin.getArenaHandler().getArenas()) {
            activeMatches.add(arena.getMatch());
        }
        Iterator<Map.Entry<Match, Scoreboard>> iterator = boards.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Match, Scoreboard> entry = iterator.next();
            if (!activeMatches.contains(entry.getKey())) {
                iterator.remove();
            }
        }
    }


    /**
     * Show the player's health under their nameplate
     */
    private void createPlayerHealthObjective(Match match) {

        Scoreboard board = boards.get(match);
        Objective objective = board.registerNewObjective("playerhealth", "health");
        objective.setDisplaySlot(DisplaySlot.BELOW_NAME);
        objective.setDisplayName(ChatColor.RED + "❤");

        //Force health objective to update health readout
        for (Team team : match.getTeams().values()) {
            for (Player player : team.getPlayers()) {
                player.setHealth(player.getHealth());
            }
        }

    }


    /**
     * Set up the sidebar score box
     */
    private void createSidebarTeamScoreObjective(Match match) {

        Scoreboard board = boards.get(match);
        Objective objective = board.registerNewObjective("matchscore", "dummy");
        objective.setDisplaySlot(DisplaySlot.SIDEBAR);
        objective.setDisplayName(String.format("Match %s(00:00)", ChatColor.AQUA));

        // Create score lines for each team
        for (Team team : match.getTeams().values()) {
            if (!team.isSpectator()) {
                Score score = objective.getScore(team.getChatColor() + team.getName());
                score.setScore(0);
            }
        }

    }


    /**
     * Update the match time display in the sidebar
     */
    public void updateMatchTimeDisplay(Match match, long secondsLeft) {
        if (!boards.containsKey(match)) return;
        long sec = secondsLeft % 60;
        long min = (secondsLeft / 60) % 60;
        String secString = String.format("%02d", sec);
        String minString = String.format("%02d", min);
        Objective objective = boards.get(match).getObjective("matchscore");
        objective.setDisplayName(String.format("Match %s(%s:%s)", ChatColor.AQUA, minString, secString));
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/chat/ChatModule.java
public class ChatModule implements Module {


    private AthenaGM plugin;

    public String getModuleName() {
        return "chat";
    }


    public ChatModule(AthenaGM plugin) {
        this.plugin = plugin;
    }


    public void unload() {}


    /**
     * Intercept chat and apply changes.
     * 1. Limit messages to the current world, so they don't across arenas or the hub
     * 2. Apply team color to names if applicable
     */
    @EventHandler
    public void onPlayerChat(AsyncPlayerChatEvent event) {

        event.setCancelled(true);

        String tag;
        Team team = PlayerUtil.getTeamForPlayer(plugin.getArenaHandler(), event.getPlayer());
        if (team != null) {
            tag = String.format("<%s%s%s> ", team.getChatColor(), event.getPlayer().getName(), ChatColor.RESET);
        } else {
            tag = String.format("<%s%s%s> ", ChatColor.GRAY, event.getPlayer().getName(), ChatColor.RESET);
        }

        for (Player player : event.getPlayer().getWorld().getPlayers()) {
            player.sendMessage(tag + event.getMessage());
        }

    }


    /**
     * Send a team chat message.
     * This does the heavy lifting when called by the /t command executor.
     * @param player The sender
     * @param message The message
     * @return Whether the message was successfully sent
     */
    public boolean sendTeamMessage(Player player, String message) {

        Team team = PlayerUtil.getTeamForPlayer(plugin.getArenaHandler(), player);
        if (team == null) return false;
        String tag = String.format("[%s%s]<%s> ", team.getColoredName(), ChatColor.RESET, player.getName());

        for (Player p : team.getPlayers()) {
            p.sendMessage(tag + message);
        }

        return true;

    }


}
src/main/java/io/github/redwallhp/athenagm/modules/worldBorder/WorldBorderModule.java
public class WorldBorderModule implements Module {


    private AthenaGM plugin;


    public String getModuleName() {
        return "worldBorder";
    }


    public WorldBorderModule(AthenaGM plugin) {
        this.plugin = plugin;
    }


    public void unload() {}


    @EventHandler
    public void onMatchCreate(MatchCreateEvent event) {
        GameMap map = event.getMatch().getMap();
        if (map.getWorldBorder().isEnabled()) {
            WorldBorder wb = event.getMatch().getWorld().getWorldBorder();
            wb.setCenter(map.getWorldBorder().getCenterX(), map.getWorldBorder().getCenterY());
            wb.setSize(map.getWorldBorder().getRadius()*2);
        }
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/friendlyFire/FriendlyFireModule.java
public class FriendlyFireModule implements Module {


    private AthenaGM plugin;
    private HashMap<UUID, Boolean> exemptions;


    public String getModuleName() {
        return "friendlyFire";
    }


    public FriendlyFireModule(AthenaGM plugin) {
        this.plugin = plugin;
        this.exemptions = new HashMap<UUID, Boolean>();
    }


    public void unload() {}


    /**
     * Block friendly fire
     */
    @EventHandler(priority = EventPriority.LOW)
    public void blockFriendlyFire(PlayerDamagePlayerEvent event) {
        if (event.isFriendlyFire() && !isMatchExempt(event.getMatch())) {
            event.setCancelled(true);
        }
    }


    /**
     * Stop negative effect splash potions from hurting a player's own team
     */
    @EventHandler(priority = EventPriority.LOW)
    public void blockFriendlyFirePotions(PotionSplashEvent event) {

        Set<PotionEffectType> blacklist = new HashSet<>();
        blacklist.add(PotionEffectType.HARM);
        blacklist.add(PotionEffectType.POISON);
        blacklist.add(PotionEffectType.WEAKNESS);
        blacklist.add(PotionEffectType.SLOW);
        blacklist.add(PotionEffectType.INVISIBILITY);

        if (!(event.getPotion().getShooter() instanceof Player)) return;
        Player damager = (Player) event.getPotion().getShooter();
        Arena arena = plugin.getArenaHandler().getArenaForPlayer(damager);

        if (arena != null) {
            Team dTeam = PlayerUtil.getTeamForPlayer(arena.getMatch(), damager);
            for (LivingEntity ent : event.getAffectedEntities()) {
                if (!(ent instanceof Player)) continue;
                if (dTeam == PlayerUtil.getTeamForPlayer(arena.getMatch(), (Player) ent)) {
                    for (PotionEffect effect : event.getPotion().getEffects()) {
                        if (blacklist.contains(effect.getType())) event.setIntensity(ent, 0);
                    }
                }
            }
        }

    }


    /**
     * Clean up the exemptions list so we don't infinitely expand the HashMap
     */
    @EventHandler
    public void onMatchStateChanged(MatchStateChangedEvent event) {
        if (event.getCurrentState() == MatchState.ENDED) {
            exemptions.remove(event.getMatch().getUUID());
        }
    }


    /**
     * Set whether a given Match will allow friendly fire
     * @param match The Match
     * @param value True if friendly fire will be allowed
     */
    public void setMatchExempt(Match match, boolean value) {
        exemptions.put(match.getUUID(), value);
    }


    /**
     * Returns true if a Match allows friendly fire
     * @param match The Match to check
     */
    public boolean isMatchExempt(Match match) {
        return (exemptions.containsKey(match.getUUID()) && exemptions.get(match.getUUID()));
    }


}
src/main/java/io/github/redwallhp/athenagm/AthenaGM.java
public class AthenaGM extends JavaPlugin {


    public Configuration config;
    private ArenaHandler arenaHandler;
    private ModuleLoader moduleLoader;
    private RegionHandler regionHandler;
    private Hub hub;


    @Override
    public void onEnable() {
        this.config = new Configuration(this);
        setupDirectories();
        this.moduleLoader = new ModuleLoader(this);
        this.regionHandler = new RegionHandler(this);
        this.arenaHandler = new ArenaHandler(this);
        this.hub = new Hub(this);
        new Tracker(this);
        registerCommands();
    }


    @Override
    public void onDisable() {
        this.moduleLoader.unload();
    }


    private void registerCommands() {
        AdminCommands adminCommands = new AdminCommands(this);
        ArenaCommands arenaCommands = new ArenaCommands(this);
        MatchCommands matchCommands = new MatchCommands(this);
    }


    private void setupDirectories() {
        getMapsDirectory().mkdir();
        getMatchesDirectory().mkdir();
    }


    /**
     * Exposes the plugin's void world generator for Spigot and other plugins to use
     */
    @Override
    public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
        return new VoidGenerator();
    }


    /**
     * Returns a File object representing the directory maps are stored in
     */
    public File getMapsDirectory() {
        return new File("maps");
    }


    /**
     * Returns a File object representing the directory where instanced maps for matches are stored
     */
    public File getMatchesDirectory() {
        return new File("matches");
    }


    /**
     * Get a File object representing the AthenaGM plugin directory.
     * Mostly a convenience method for other plugins.
     */
    public File getPluginDirectory() {
        return getDataFolder();
    }


    /**
     * Get a reference to the ArenaHandler, to look up active arenas or player-arena membership
     */
    public ArenaHandler getArenaHandler() {
        return arenaHandler;
    }


    /**
     * Get a reference to the RegionHandler
     */
    public RegionHandler getRegionHandler() {
        return regionHandler;
    }


    /**
     * Get a reference to a loaded Module
     * @param name The name of the Module, as returned by its getModuleName() method
     * @return A Module object, which can be cast to the original class of the module to access its methods
     */
    public Module getModule(String name) {
        return this.moduleLoader.getModule(name);
    }


    /**
     * Get a reference to the Hub
     */
    public Hub getHub() {
        return hub;
    }


    /**
     * Get a reference to WorldEdit if it is present.
     * Use this to check for the plugin's presence before calling any methods from WorldEditUtil.
     */
    public WorldEditPlugin getWE() {
        Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin("WorldEdit");
        if (plugin == null || !(plugin instanceof WorldEditPlugin)) {
            return null;
        }
        return (WorldEditPlugin) plugin;
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/playerFreeze/PlayerFreezeModule.java
public class PlayerFreezeModule implements Module {


    private AthenaGM plugin;


    public String getModuleName() {
        return "playerFreeze";
    }


    public PlayerFreezeModule(AthenaGM plugin) {
        this.plugin = plugin;
    }


    public void unload() {}


    /**
     * The condition setting whether a player should be frozen
     * @param player The player to evaluate
     */
    private boolean shouldFreeze(Player player) {
        Team team = PlayerUtil.getTeamForPlayer(plugin.getArenaHandler(), player);
        return team != null && (!team.isSpectator() && team.getMatch().getState() != MatchState.PLAYING);
    }


    /**
     * Handle movement freezing
     */
    private void setMovement(Player player) {
        if (shouldFreeze(player)) {
            player.setWalkSpeed(0);
            player.addPotionEffect(new PotionEffect(PotionEffectType.JUMP, Integer.MAX_VALUE, 128, false));
        } else {
            player.setWalkSpeed(0.2f);
            player.removePotionEffect(PotionEffectType.JUMP);
        }
    }


    /**
     * Freeze/unfreeze when the MatchState changes to and from PLAYING
     */
    @EventHandler
    public void onMatchStateChanged(MatchStateChangedEvent event) {
        for (Map.Entry<Player, Team> entry: event.getMatch().getPlayerTeamMap().entrySet()) {
            setMovement(entry.getKey());
        }
    }


    /**
     * Freeze/unfreeze when changing teams
     */
    @EventHandler
    public void onPlayerChangedTeam(PlayerChangedTeamEvent event) {
        setMovement(event.getPlayer());
    }


    /**
     * Ensure no movement at all when frozen
     */
    @EventHandler
    public void onPlayerMove(PlayerMoveEvent event) {
        if (shouldFreeze(event.getPlayer())) {
            if (event.getTo().getX() != event.getFrom().getX() || event.getTo().getZ() != event.getFrom().getZ()) {
                event.setCancelled(true);
            }
        }
    }


    /**
     * Stop frozen players from using melee combat
     */
    @EventHandler
    public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
        if (event.getDamager() instanceof Player) {
            Player player = (Player) event.getDamager();
            if (shouldFreeze(player)) {
                event.setCancelled(true);
            }
        }
    }


    /**
     * Stop frozen players from firing bows
     */
    @EventHandler
    public void onEntityShootBow(EntityShootBowEvent event) {
        if (event.getEntity() instanceof Player) {
            Player player = (Player) event.getEntity();
            if (shouldFreeze(player)) {
                event.setCancelled(true);
            }
        }
    }


    /**
     * Stop frozen players from throwing splash potions, eating, or otherwise using items
     */
    @EventHandler
    public void onPlayerInteractEvent(PlayerInteractEvent event) {
        if (shouldFreeze(event.getPlayer()) &&event.getItem() != null) {
            event.setCancelled(true);
            event.getPlayer().updateInventory();
        }
    }


    /**
     * Stop splash potions from splashing
     */
    @EventHandler
    public void onPotionsSplash(PotionSplashEvent event){
        if (event.getEntity().getShooter() instanceof Player) {
            Player player = (Player) event.getEntity().getShooter();
            if (shouldFreeze(player)) {
                event.setCancelled(true);
            }
        }
    }


    /**
     * Block dropping items
     */
    @EventHandler
    public void onPlayerDropitem(PlayerDropItemEvent event) {
        if (shouldFreeze(event.getPlayer())) {
            event.setCancelled(true);
            event.getPlayer().updateInventory();
        }
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/spectator/SpectatorModule.java
public class SpectatorModule implements Module {


    private AthenaGM plugin;
    private HashMap<Player, Integer> compassIndex;
    private ItemStack helpBookItem;


    public String getModuleName() {
        return "spectator";
    }


    public SpectatorModule(AthenaGM plugin) {
        this.plugin = plugin;
        this.compassIndex = new HashMap<Player, Integer>();
        this.helpBookItem = getHelpBookItem();
    }


    public void unload() {}


    @EventHandler(priority = EventPriority.HIGHEST)
    public void handleSpectatorMode(PlayerMatchRespawnEvent event) {

        Player player = event.getPlayer();
        Match match = plugin.getArenaHandler().getArenaForPlayer(player).getMatch();
        Team playerTeam = PlayerUtil.getTeamForPlayer(match, player);

        // Set spectator/non-spectator attributes
        if (playerTeam != null && playerTeam.isSpectator()) {
            player.setGameMode(GameMode.CREATIVE);
            player.spigot().setCollidesWithEntities(false);
            player.setCanPickupItems(false);
            this.setPlayerVisibility(player, true, match);
        } else {
            player.setGameMode(GameMode.SURVIVAL);
            player.spigot().setCollidesWithEntities(true);
            player.setCanPickupItems(true);
            this.setPlayerVisibility(player, false, match);
        }

    }


    @EventHandler
    public void giveSpectatorKit(PlayerMatchRespawnEvent event) {

        if (!isPlayerSpectator(event.getPlayer())) return;
        Inventory inventory = event.getPlayer().getInventory();

        ItemStack compass = new ItemStack(Material.COMPASS, 1);
        ItemMeta compassMeta = compass.getItemMeta();
        compassMeta.setDisplayName(ChatColor.BLUE + "Teleport");
        compass.setItemMeta(compassMeta);
        inventory.addItem(compass);

        ItemStack helmet = new ItemStack(Material.LEATHER_HELMET, 1);
        ItemMeta helmetMeta = helmet.getItemMeta();
        helmetMeta.setDisplayName(ChatColor.GOLD + "Select Team");
        helmet.setItemMeta(helmetMeta);
        inventory.addItem(helmet);

        inventory.addItem(this.helpBookItem);

        ItemStack exitDoor = new ItemStack(Material.WOOD_DOOR, 1);
        ItemMeta exitDoorMeta = exitDoor.getItemMeta();
        exitDoorMeta.setDisplayName(ChatColor.AQUA + "Return to Minigames Hub");
        exitDoor.setItemMeta(exitDoorMeta);
        inventory.setItem(8, exitDoor);

    }


    @EventHandler
    public void onPlayerInteract(PlayerInteractEvent event) {

        if (!isPlayerSpectator(event.getPlayer())) return;

        // Block all pressure plate events early
        if (event.getAction() == Action.PHYSICAL) {
            event.setCancelled(true);
            return;
        }

        // Player is using the Help Book
        if (event.getPlayer().getItemInHand() != null && event.getPlayer().getItemInHand().getType() == Material.WRITTEN_BOOK) {
            if (event.getAction() == Action.RIGHT_CLICK_BLOCK && ItemUtil.getInteractiveBlocks().contains(event.getClickedBlock().getType())) {
                event.setCancelled(true);
            }
            return;
        }

        // Player is using team selector helmet
        if (event.getPlayer().getItemInHand() != null && event.getPlayer().getItemInHand().getType() == Material.LEATHER_HELMET) {
            event.setCancelled(true);
            event.getPlayer().updateInventory();
            event.getPlayer().openInventory(getTeamPickerInventory(event.getPlayer()));
            return;
        }

        // Player is using the navigation compass
        if (event.getPlayer().getItemInHand() != null && event.getPlayer().getItemInHand().getType() == Material.COMPASS) {
            event.setCancelled(true);
            doCompassTeleport(event);
            return;
        }

        // Player is using the exit door
        if (event.getPlayer().getItemInHand() != null && event.getPlayer().getItemInHand().getType() == Material.WOOD_DOOR) {
            event.setCancelled(true);
            plugin.getHub().spawnPlayer(event.getPlayer());
            event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ENDERMEN_TELEPORT, 1f, 1f);
            return;
        }

        // Cancel all other interact events
        event.setCancelled(true);

    }


    @EventHandler
    public void onInventoryClick(InventoryClickEvent event) {
        Player player = (Player) event.getWhoClicked();
        ItemStack item = event.getCurrentItem();
        if (!isPlayerSpectator(player)) return;
        if (item != null) {
            if (event.getInventory().getTitle().equals("Select Team")) {
                if (item.getType().equals(Material.LEATHER_HELMET)) {
                    String teamName = ChatColor.stripColor(item.getItemMeta().getDisplayName()).toLowerCase();
                    event.setCancelled(true);
                    player.closeInventory();
                    player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1, 2);
                    Bukkit.dispatchCommand(player, String.format("team %s", teamName));
                    return;
                }
                if (item.getType().equals(Material.STONE_BUTTON)) {
                    event.setCancelled(true);
                    player.closeInventory();
                    player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1, 2);
                    Bukkit.dispatchCommand(player, "autojoin");
                    return;
                }
            }
        }
        event.setCancelled(true);
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onBlockPlace(BlockPlaceEvent event) {
        if (!isPlayerSpectator(event.getPlayer())) return;
        event.setCancelled(true);
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onBlockBreak(BlockBreakEvent event) {
        if (!isPlayerSpectator(event.getPlayer())) return;
        event.setCancelled(true);
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onPlayerDropItem(PlayerDropItemEvent event) {
        if (!isPlayerSpectator(event.getPlayer())) return;
        if (event.getPlayer().getOpenInventory() != null) {
            event.setCancelled(true);
        } else {
            event.getPlayer().setItemInHand(event.getItemDrop().getItemStack());
            event.getItemDrop().remove();
        }
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onPlayerPickupItem(PlayerPickupItemEvent event) {
        if (!isPlayerSpectator(event.getPlayer())) return;
        event.setCancelled(true);
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onPlayerPickupArrow(PlayerPickupArrowEvent event) {
        if (!isPlayerSpectator(event.getPlayer())) return;
        event.setCancelled(true);
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
        if (event.getDamager() instanceof Player) {
            Player damager = (Player) event.getDamager();
            if (isPlayerSpectator(damager)) {
                event.setCancelled(true);
            }
        }
    }


    @EventHandler(priority = EventPriority.HIGHEST)
    public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
        if (!isPlayerSpectator(event.getPlayer())) return;
        event.setCancelled(true);
    }


    @EventHandler
    public void onVehicleEnter(VehicleEnterEvent event) {
        if (event.getEntered() instanceof Player) {
            Player player = (Player) event.getEntered();
            if (isPlayerSpectator(player)) {
                event.setCancelled(true);
            }
        }
    }


    @EventHandler
    public void onVehicleDamage(VehicleDamageEvent event) {
        if (event.getAttacker() instanceof Player) {
            Player player = (Player) event.getAttacker();
            if (isPlayerSpectator(player)) {
                event.setCancelled(true);
            }
        }
    }


    private Inventory getTeamPickerInventory(Player player) {
        Match match = plugin.getArenaHandler().getArenaForPlayer(player).getMatch();
        Inventory inventory = Bukkit.createInventory(null, 9, "Select Team");
        for (Team team : match.getTeams().values()) {
            if (!team.isSpectator()) {
                ItemStack item = new ItemStack(Material.LEATHER_HELMET, 1);
                LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta();
                meta.setColor(ItemUtil.getColorFormString(team.getColor()));
                String name = String.format("%s%s", team.getChatColor(), team.getName());
                meta.setDisplayName(name);
                meta.setLore(Arrays.asList(String.format("%d/%d", team.getPlayers().size(), team.getSize())));
                item.setItemMeta(meta);
                inventory.addItem(item);
            }
        }
        ItemStack item = new ItemStack(Material.STONE_BUTTON, 1);
        ItemMeta meta = item.getItemMeta();
        meta.setDisplayName("Auto Assign");
        item.setItemMeta(meta);
        inventory.addItem(item);
        return inventory;
    }


    private void doCompassTeleport(PlayerInteractEvent event) {
        if (event.getAction() == Action.LEFT_CLICK_BLOCK  || event.getAction() == Action.LEFT_CLICK_AIR) {
            // Teleport to the next player
            Match match = plugin.getArenaHandler().getArenaForPlayer(event.getPlayer()).getMatch();
            List<Player> players = new ArrayList<Player>();
            for (Team team : match.getTeams().values()) {
                if (team.isSpectator()) continue;
                for (Player player : team.getPlayers()) {
                    players.add(player);
                }
            }
            Integer nextIndex = 0;
            if (this.compassIndex.containsKey(event.getPlayer())) {
                nextIndex = this.compassIndex.get(event.getPlayer()) + 1;
                if (nextIndex > players.size() - 1) nextIndex = 0;
                this.compassIndex.put(event.getPlayer(), nextIndex);
            } else {
                this.compassIndex.put(event.getPlayer(), 0);
            }
            if (players.size() > 0) {
                event.getPlayer().teleport(players.get(nextIndex).getLocation());
                event.getPlayer().sendMessage(String.format("%sTeleporting to %s", ChatColor.GRAY, players.get(nextIndex).getName()));
            }
        }
        if (event.getAction() == Action.RIGHT_CLICK_BLOCK || event.getAction() == Action.RIGHT_CLICK_AIR) {
            // Teleport back to spawn point
            Match match = plugin.getArenaHandler().getArenaForPlayer(event.getPlayer()).getMatch();
            event.getPlayer().teleport(match.getSpawnPoint(event.getPlayer()));
        }
    }


    public ItemStack getHelpBookItem() {
        File file = new File(plugin.getDataFolder(), "helpbook.txt");
        BookBuilder bookBuilder = new BookBuilder("Help");
        bookBuilder.setDefaultContents("This book will be populated with the contents of a &lhelpbook.txt&0 file in the plugin directory.");
        bookBuilder.setPagesFromFile(file);
        this.helpBookItem = bookBuilder.getBook(); //cache to avoid hitting the disk all the time
        return bookBuilder.getBook();
    }


    private void setPlayerVisibility(Player player, boolean playerIsSpectator, Match match) {

        HashMap<Player, Team> teamMap = match.getPlayerTeamMap();

        // Handle respawning player
        for (Map.Entry<Player, Team> entry : teamMap.entrySet()) {
            Player lPlayer = entry.getKey();
            Team lTeam = entry.getValue();
            if (playerIsSpectator) {
                // Show all other players to the player respawning
                player.showPlayer(lPlayer);
            } else {
                // Hide spectators from the player respawning, but show all others
                if (lTeam.isSpectator()) {
                    player.hidePlayer(lPlayer);
                } else {
                    player.showPlayer(lPlayer);
                }
            }
        }

        // Handle other players
        for (Map.Entry<Player, Team> entry : teamMap.entrySet()) {
            Player lPlayer = entry.getKey();
            Team lTeam = entry.getValue();
            if (playerIsSpectator) {
                // Hide the respawning player from others who are not on the spectator team
                if (lTeam.isSpectator()) {
                    lPlayer.showPlayer(player);
                } else {
                    lPlayer.hidePlayer(player);
                }
            } else {
                lPlayer.showPlayer(player);
            }
        }

    }


    private boolean isPlayerSpectator(Player player) {
        Team team = PlayerUtil.getTeamForPlayer(plugin.getArenaHandler(), player);
        return (team != null && team.isSpectator());
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/voting/VotingModule.java
public class VotingModule implements Module {


    private AthenaGM plugin;
    private Map<Arena, Vote> votes;
    private BukkitRunnable ticker;


    public String getModuleName() {
        return "voting";
    }


    public VotingModule(AthenaGM plugin) {
        this.plugin = plugin;
        this.votes = new HashMap<Arena, Vote>();
        this.ticker = new BukkitRunnable() {
            public void run() {
                Iterator<Vote> it = votes.values().iterator();
                while (it.hasNext()) {
                    Vote vote = it.next();
                    boolean rem = vote.tick();
                    if (rem) {
                        it.remove();
                    }
                }
            }
        };
        this.ticker.runTaskTimer(plugin, 20L, 20L);
    }


    public void unload() {
        this.ticker.cancel();
    }


    /**
     * Create a vote for an immediate map change.
     * @param arena the Arena this vote originates from
     * @param mapName the name of the map to vote for, or "random"
     * @return true if the vote was successfully started
     */
    public boolean createMapVote(Arena arena, Player player, String mapName) {

        if (!plugin.config.VOTING) {
            player.sendMessage(ChatColor.RED + "Map voting is not enabled.");
            return false;
        }

        if (votes.containsKey(arena)) {
            player.sendMessage(ChatColor.RED + "Only one vote can be active at once!");
            return false;
        }

        if (arena.getMatch().getTimer().timeLeftInSeconds() <= 120) {
            player.sendMessage(ChatColor.RED + "You cannot start a map vote this late in the match.");
            return false;
        }

        Team team = PlayerUtil.getTeamForPlayer(arena.getMatch(), player);
        if (team == null || team.isSpectator()) {
            player.sendMessage(ChatColor.RED + "Spectators cannot vote!");
            return false;
        }

        // Determine map
        GameMap map = null;
        if (mapName.equalsIgnoreCase("random")) {
            List<GameMap> mapList = arena.getRotation().getMaps();
            Random random = new Random();
            int roll = random.nextInt(mapList.size());
            map = mapList.get(roll);
        } else {
            map = arena.getRotation().getMapByFileName(mapName);
        }

        // Fail if a valid map was not specified
        if (map == null) {
            List<String> fileNames = new ArrayList<String>();
            for (GameMap m : arena.getRotation().getMaps()) {
                fileNames.add(m.getFileName());
            }
            String maps = StringUtil.joinList(", ", fileNames);
            player.sendMessage(String.format("%sMap not found!%s Valid maps: %s", ChatColor.RED, ChatColor.GRAY, maps));
            return false;
        }

        // Instantiate vote
        Vote vote = new Vote(arena, map);
        votes.put(arena, vote);
        vote.cast(player, "Yes");

        // Broadcast the vote text
        arena.getMatch().broadcast(String.format("%s[Vote]%s %s has started a vote. Do you want to switch maps to %s?", ChatColor.YELLOW, ChatColor.DARK_AQUA, player.getName(), mapName));
        String yesStr = String.format("%s/vote yes%s", ChatColor.YELLOW, ChatColor.GRAY);
        String noStr = String.format("%s/vote no%s", ChatColor.YELLOW, ChatColor.GRAY);
        arena.getMatch().broadcast(String.format("%sType %s or %s to vote!", ChatColor.GRAY, yesStr, noStr));

        return true;

    }


    /**
     * Create a vote for the map that will follow the current one
     * @param arena the Arena this vote originates from
     * @param player the Player initiating the vote
     * @return true if the vote was successfully started
     */
    public boolean createNextMapVote(Arena arena, Player player) {

        if (!plugin.config.VOTING) {
            player.sendMessage(ChatColor.RED + "Map voting is not enabled.");
            return false;
        }

        if (votes.containsKey(arena)) {
            player.sendMessage(ChatColor.RED + "Only one vote can be active at once!");
            return false;
        }

        Team team = PlayerUtil.getTeamForPlayer(arena.getMatch(), player);
        if (team == null || team.isSpectator()) {
            player.sendMessage(ChatColor.RED + "Spectators cannot vote!");
            return false;
        }

        // Pick three random maps
        List<GameMap> mapList = new ArrayList<>(arena.getRotation().getMaps());
        List<String> mapPicks = new ArrayList<>();
        mapList.remove(arena.getMatch().getMap());
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            int roll = random.nextInt(mapList.size());
            mapPicks.add(mapList.get(roll).getFileName());
            mapList.remove(roll);
        }

        // Instantiate vote
        votes.put(arena, new Vote(arena, mapPicks));

        // Broadcast the vote text
        arena.getMatch().broadcast(String.format("%s[Vote]%s %s has started a vote to set the upcoming map.", ChatColor.YELLOW, ChatColor.DARK_AQUA, player.getName()));
        arena.getMatch().broadcast(String.format("%sThree random maps have been selected. Type one of the following to vote:", ChatColor.GRAY));
        for (String map : mapPicks) {
            arena.getMatch().broadcast(String.format("%s- %s/vote %s", ChatColor.GRAY, ChatColor.YELLOW, map));
        }

        return true;

    }


    /**
     * Process a player's vote
     * @param arena the arena the ballot is in
     * @param player the player casting the vote
     * @param choice the player's response
     */
    public void vote(Arena arena, Player player, String choice) {

        if (!votes.containsKey(arena)) {
            player.sendMessage(ChatColor.RED + "There is not an ongoing vote!");
            return;
        }

        Vote vote = votes.get(arena);
        Team team = PlayerUtil.getTeamForPlayer(arena.getMatch(), player);

        if (vote.hasVoted(player.getUniqueId())) {
            player.sendMessage(ChatColor.RED + "You have already voted!");
            return;
        }

        if (team == null || team.isSpectator()) {
            player.sendMessage(ChatColor.RED + "Spectators cannot vote!");
            return;
        }

        // Handle vote
        boolean success = vote.cast(player, choice);
        if (success) {
            player.sendMessage(ChatColor.GREEN + "Your vote was cast!");
        } else {
            String options = StringUtil.joinList(", ", new ArrayList<String>(vote.getOptions().keySet()));
            player.sendMessage(String.format("%sInvalid vote!%s Valid options: %s", ChatColor.RED, ChatColor.GRAY, options));
        }

    }


}
src/main/java/io/github/redwallhp/athenagm/modules/deathMessage/DeathMessageModule.java
public class DeathMessageModule implements Module {


    private AthenaGM plugin;


    public String getModuleName() {
        return "deathMessage";
    }


    public DeathMessageModule(AthenaGM plugin) {
        this.plugin = plugin;
    }


    public void unload() {}


    /**
     * Suppress regular death messages, replacing them with custom ones local to the player's Match
     */
    @EventHandler
    public void onAthenaDeath(AthenaDeathEvent event) {
        DamageCause cause = event.getPlayer().getLastDamageCause().getCause();
        if (event.isPvP()) {
            switch (cause) {
                case FALL:
                    handleGroundFall(event);
                    break;
                case PROJECTILE:
                    handleProjectile(event);
                    break;
                case VOID:
                    handleVoid(event);
                    break;
                default:
                    broadcast(event, event.getDeathEvent().getDeathMessage());
            }
        } else {
            broadcast(event, event.getDeathEvent().getDeathMessage());
        }
        event.getDeathEvent().setDeathMessage(null); //disable vanilla death broadcast
    }


    /**
     * Death messages for the player hitting the ground
     */
    private void handleGroundFall(AthenaDeathEvent event) {
        String p = event.getPlayer().getName();
        String k = event.getKiller().getName();
        DamageCause cause = event.getDamageEvent().getEntityEvent().getCause();
        if (cause.equals(DamageCause.PROJECTILE)) {
            int dist = Math.round(event.getDamageEvent().getDistance());
            broadcast(event, String.format("%s was shot off a high place by %s (%dm)", p, k, dist));
        }
        else {
            broadcast(event, String.format("%s was knocked off a high place by %s", p, k));
        }
    }


    /**
     * Death messages for the player being shot
     */
    private void handleProjectile(AthenaDeathEvent event) {
        String p = event.getPlayer().getName();
        String k = event.getKiller().getName();
        int dist = Math.round(event.getDamageEvent().getDistance());
        broadcast(event, String.format("%s was shot by %s (%dm)", p, k, dist));
    }


    /**
     * Death messages for the player falling into the void
     */
    private void handleVoid(AthenaDeathEvent event) {
        String p = event.getPlayer().getName();
        String k = event.getKiller().getName();
        DamageCause cause = event.getDamageEvent().getEntityEvent().getCause();
        if (cause.equals(DamageCause.PROJECTILE)) {
            int dist = Math.round(event.getDamageEvent().getDistance());
            broadcast(event, String.format("%s was shot into the void by %s (%dm)", p, k, dist));
        }
        else {
            broadcast(event, String.format("%s was knocked into the void by %s", p, k));
        }
    }


    /**
     * Broadcast custom death message after colorizing it
     * @param event the AthenaDeathEvent we caught
     * @param msg the message to broadcast
     */
    private void broadcast(AthenaDeathEvent event, String msg) {
        msg = ChatColor.stripColor(msg);
        msg = ChatColor.GRAY + msg;
        msg = msg.replace(event.getPlayer().getName(), event.getPlayerTeam().getChatColor() + event.getPlayer().getName() + ChatColor.GRAY);
        if (event.isPvP()) {
            msg = msg.replace(event.getKiller().getName(), event.getKillerTeam().getChatColor() + event.getKiller().getName() + ChatColor.GRAY);
        }
        event.getMatch().broadcast(msg);
    }


}
src/main/java/io/github/redwallhp/athenagm/modules/broadcasts/BroadcastsModule.java
public class BroadcastsModule implements Module {


    private AthenaGM plugin;
    private List<String> messages;


    public String getModuleName() {
        return "broadcasts";
    }


    public BroadcastsModule(AthenaGM plugin) {
        this.plugin = plugin;
        this.messages = new ArrayList<String>();
        loadMessages();
        startMessageCycle();
    }


    public void unload() {}


    private void loadMessages() {
        File file = new File(plugin.getDataFolder(), "broadcasts.txt");
        if (!file.exists()) return;
        try {
            BufferedReader reader = new BufferedReader(new FileReader(file));
            String line = reader.readLine();
            while (line != null) {
                messages.add(ChatColor.translateAlternateColorCodes('&', line));
                line = reader.readLine();
            }
            reader.close();
        } catch(IOException ex) {
            ex.printStackTrace();
        }
    }


    private void startMessageCycle() {
        if (messages.size() < 1) return;
        BukkitScheduler scheduler = Bukkit.getServer().getScheduler();
        scheduler.scheduleSyncRepeatingTask(plugin, new Runnable() {
            public void run() {
                printMessage();
            }
        }, 1200L, 6000L);
    }


    private void printMessage() {
        String msg = messages.get(new Random().nextInt(messages.size()));
        String fMsg;
        for (Player player : Bukkit.getServer().getOnlinePlayers()) {
            fMsg = msg.replaceAll("%player%", player.getName());
            player.sendMessage(String.format("%s\u24D8%s %s", ChatColor.DARK_PURPLE, ChatColor.LIGHT_PURPLE, fMsg));
        }
    }


}


Question: package io.github.redwallhp.athenagm.modules;
import io.github.redwallhp.athenagm.AthenaGM;
import io.github.redwallhp.athenagm.modules.broadcasts.BroadcastsModule;
import io.github.redwallhp.athenagm.modules.chat.ChatModule;
import io.github.redwallhp.athenagm.modules.deathMessage.DeathMessageModule;
import io.github.redwallhp.athenagm.modules.friendlyFire.FriendlyFireModule;
import io.github.redwallhp.athenagm.modules.kits.KitsModule;
import io.github.redwallhp.athenagm.modules.permissions.PermissionsModule;
import io.github.redwallhp.athenagm.modules.playerFreeze.PlayerFreezeModule;
import io.github.redwallhp.athenagm.modules.scoreboard.ScoreboardModule;
import io.github.redwallhp.athenagm.modules.spectator.SpectatorModule;
import io.github.redwallhp.athenagm.modules.voting.VotingModule;
import io.github.redwallhp.athenagm.modules.worldBorder.WorldBorderModule;
import java.util.HashMap;




/**
 * Does what it says on the box: builds and loads modules,
 * registering their event listeners as it does so.
 */
public class ModuleLoader {


    /**
     * List of module classes to load
     */
    private static Class[] moduleClasses = {
            PermissionsModule.class,
            SpectatorModule.class,
            KitsModule.class,
            ScoreboardModule.class,
            FriendlyFireModule.class,
            BroadcastsModule.class,
            PlayerFreezeModule.class,
            DeathMessageModule.class,
            WorldBorderModule.class,
            ChatModule.class,
Answer:             VotingModule.class