From 84249640d1cc1e028584f34b7e5110452fc7e893 Mon Sep 17 00:00:00 2001 From: Andreas Troelsen Date: Fri, 1 Jun 2018 23:25:11 +0200 Subject: [PATCH] Add support for join, leave, and info signs. The ability to execute commands by hitting signs is already implemented by other plugins, but by creating built-in support for such signs, it's possible to leverage information about the plugin and its current state. This implementation allows for displaying live information about player counts, waves, etc. on the signs in addition to tying actions to them. Customizable templates defined in the new signs.yml config-file can be bound to signs during the in-game sign creation, and users can define state-specific templates that change based on whether an arena is completely idle, has players in the lobby, or is running and in full swing. Sign data is stored in data/signs.data as a YAML-formatted file that shouldn't be modified directly, effectively separating configuration (templates in signs.yml) and data (coordinates and parameters in signs.data). Closes #385 --- .../garbagemule/MobArena/ArenaMasterImpl.java | 1 + .../com/garbagemule/MobArena/MobArena.java | 30 ++++ .../garbagemule/MobArena/signs/ArenaSign.java | 47 ++++++ .../MobArena/signs/HandlesSignClicks.java | 52 +++++++ .../MobArena/signs/HandlesSignCreation.java | 65 ++++++++ .../signs/HandlesSignDestruction.java | 33 ++++ .../MobArena/signs/InvokesSignAction.java | 50 ++++++ .../MobArena/signs/LoadsSignStore.java | 46 ++++++ .../MobArena/signs/LoadsTemplateStore.java | 106 +++++++++++++ .../MobArena/signs/RedrawsArenaSigns.java | 61 ++++++++ .../MobArena/signs/RedrawsSignsOnUpdates.java | 65 ++++++++ .../MobArena/signs/RemovesSignAtLocation.java | 26 ++++ .../MobArena/signs/RendersTemplate.java | 76 +++++++++ .../MobArena/signs/RendersTemplateById.java | 36 +++++ .../MobArena/signs/SavesSignStore.java | 32 ++++ .../garbagemule/MobArena/signs/SetsLines.java | 22 +++ .../MobArena/signs/SignBootstrap.java | 130 ++++++++++++++++ .../MobArena/signs/SignListeners.java | 66 ++++++++ .../garbagemule/MobArena/signs/SignStore.java | 49 ++++++ .../MobArena/signs/StoresNewSign.java | 56 +++++++ .../garbagemule/MobArena/signs/Template.java | 78 ++++++++++ .../MobArena/signs/TemplateStore.java | 21 +++ src/main/resources/signs.yml | 55 +++++++ .../MobArena/signs/ArenaSignTest.java | 57 +++++++ .../MobArena/signs/HandlesSignClicksTest.java | 89 +++++++++++ .../signs/HandlesSignCreationTest.java | 146 ++++++++++++++++++ .../signs/HandlesSignDestructionTest.java | 63 ++++++++ .../MobArena/signs/InvokesSignActionTest.java | 115 ++++++++++++++ .../MobArena/signs/RedrawsArenaSignsTest.java | 141 +++++++++++++++++ .../signs/RemovesSignAtLocationTest.java | 56 +++++++ .../MobArena/signs/RendersTemplateTest.java | 106 +++++++++++++ .../MobArena/signs/StoresNewSignTest.java | 126 +++++++++++++++ 32 files changed, 2102 insertions(+) create mode 100644 src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/InvokesSignAction.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SetsLines.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignListeners.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/SignStore.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/Template.java create mode 100644 src/main/java/com/garbagemule/MobArena/signs/TemplateStore.java create mode 100644 src/main/resources/signs.yml create mode 100644 src/test/java/com/garbagemule/MobArena/signs/ArenaSignTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/HandlesSignClicksTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/HandlesSignCreationTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/HandlesSignDestructionTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/InvokesSignActionTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/RedrawsArenaSignsTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/RemovesSignAtLocationTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/RendersTemplateTest.java create mode 100644 src/test/java/com/garbagemule/MobArena/signs/StoresNewSignTest.java diff --git a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java index e4a3873..4c0974f 100644 --- a/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java +++ b/src/main/java/com/garbagemule/MobArena/ArenaMasterImpl.java @@ -674,6 +674,7 @@ public class ArenaMasterImpl implements ArenaMaster plugin.reloadConfig(); config = plugin.getConfig(); initialize(); + plugin.reloadSigns(); if (wasEnabled) setEnabled(true); } diff --git a/src/main/java/com/garbagemule/MobArena/MobArena.java b/src/main/java/com/garbagemule/MobArena/MobArena.java index 60f950d..813e413 100644 --- a/src/main/java/com/garbagemule/MobArena/MobArena.java +++ b/src/main/java/com/garbagemule/MobArena/MobArena.java @@ -8,6 +8,9 @@ import com.garbagemule.MobArena.listeners.MagicSpellsListener; import com.garbagemule.MobArena.metrics.ArenaCountChart; import com.garbagemule.MobArena.metrics.ClassCountChart; import com.garbagemule.MobArena.metrics.VaultChart; +import com.garbagemule.MobArena.signs.ArenaSign; +import com.garbagemule.MobArena.signs.SignBootstrap; +import com.garbagemule.MobArena.signs.SignListeners; import com.garbagemule.MobArena.things.ThingManager; import com.garbagemule.MobArena.util.VersionChecker; import com.garbagemule.MobArena.util.config.ConfigUtils; @@ -18,6 +21,7 @@ import org.bukkit.ChatColor; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.RegisteredServiceProvider; @@ -52,6 +56,8 @@ public class MobArena extends JavaPlugin private Messenger messenger; private ThingManager thingman; + private SignListeners signListeners; + @Override public void onLoad() { thingman = new ThingManager(this); @@ -61,6 +67,7 @@ public class MobArena extends JavaPlugin // Initialize config-file configFile = new File(getDataFolder(), "config.yml"); config = new YamlConfiguration(); + ConfigurationSerialization.registerClass(ArenaSign.class); reloadConfig(); // Initialize global messenger @@ -88,6 +95,9 @@ public class MobArena extends JavaPlugin arenaMaster = new ArenaMasterImpl(this); arenaMaster.initialize(); + // Load signs after Messenger and ArenaMaster + reloadSigns(); + // Register event listeners registerListeners(); @@ -111,6 +121,7 @@ public class MobArena extends JavaPlugin } arenaMaster.resetArenaMap(); VersionChecker.shutdown(); + ConfigurationSerialization.unregisterClass(ArenaSign.class); getLogger().info("disabled."); } @@ -126,6 +137,16 @@ public class MobArena extends JavaPlugin @Override public void reloadConfig() { + // Make sure the data folder exists + File data = new File(getDataFolder(), "data"); + if (!data.exists()) { + boolean created = data.mkdir(); + if (!created) { + throw new IllegalStateException("Failed to create data folder!"); + } + getLogger().info("Created data folder."); + } + // Check if the config-file exists if (!configFile.exists()) { getLogger().info("No config-file found, creating default..."); @@ -162,6 +183,15 @@ public class MobArena extends JavaPlugin } } + void reloadSigns() { + if (signListeners != null) { + signListeners.unregister(); + } + SignBootstrap bootstrap = SignBootstrap.create(this); + signListeners = new SignListeners(); + signListeners.register(bootstrap); + } + @Override public void saveConfig() { try { diff --git a/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java b/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java new file mode 100644 index 0000000..783a189 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/ArenaSign.java @@ -0,0 +1,47 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; +import org.bukkit.configuration.serialization.ConfigurationSerializable; + +import java.util.HashMap; +import java.util.Map; + +public class ArenaSign implements ConfigurationSerializable { + + final Location location; + final String templateId; + final String arenaId; + final String type; + + ArenaSign(Location location, String templateId, String arenaId, String type) { + this.location = location; + this.templateId = templateId; + this.arenaId = arenaId; + this.type = type; + } + + @Override + public Map serialize() { + Map result = new HashMap<>(); + result.put("location", location); + result.put("templateId", templateId); + result.put("arenaId", arenaId); + result.put("type", type); + return result; + } + + @SuppressWarnings("WeakerAccess") + public static ArenaSign deserialize(Map map) { + try { + Location location = (Location) map.get("location"); + String templateId = (String) map.get("templateId"); + String arenaId = (String) map.get("arenaId"); + String type = (String) map.get("type"); + return new ArenaSign(location, templateId, arenaId, type); + } catch (ClassCastException e) { + String msg = "An arena sign in " + SignStore.FILENAME + " is invalid! You may have to delete the file."; + throw new IllegalStateException(msg); + } + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java new file mode 100644 index 0000000..07c1d3b --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignClicks.java @@ -0,0 +1,52 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.block.Block; +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +class HandlesSignClicks implements Listener { + + private static final long COOLDOWN_TIME = 500; + + private final SignStore signStore; + private final InvokesSignAction invokesSignAction; + private final Map cooldowns; + + HandlesSignClicks(SignStore signStore, InvokesSignAction invokesSignAction) { + this.signStore = signStore; + this.invokesSignAction = invokesSignAction; + this.cooldowns = new HashMap<>(); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void on(PlayerInteractEvent event) { + Block block = event.getClickedBlock(); + if (block == null) { + return; + } + if (!(block.getState() instanceof Sign)) { + return; + } + signStore.findByLocation(block.getLocation()) + .ifPresent(sign -> purgeAndInvoke(sign, event.getPlayer())); + } + + private void purgeAndInvoke(ArenaSign sign, Player player) { + long now = System.currentTimeMillis(); + cooldowns.values().removeIf(time -> time < now); + + cooldowns.computeIfAbsent(player.getUniqueId(), id -> { + invokesSignAction.invoke(sign, player); + return now + COOLDOWN_TIME; + }); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java new file mode 100644 index 0000000..a878afa --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignCreation.java @@ -0,0 +1,65 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.Messenger; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.SignChangeEvent; + +import java.util.stream.IntStream; + +class HandlesSignCreation implements Listener { + + private final StoresNewSign storesNewSign; + private final RendersTemplateById rendersTemplate; + private final Messenger messenger; + + HandlesSignCreation( + StoresNewSign storesNewSign, + RendersTemplateById rendersTemplate, + Messenger messenger + ) { + this.storesNewSign = storesNewSign; + this.rendersTemplate = rendersTemplate; + this.messenger = messenger; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void on(SignChangeEvent event) { + if (!trim(event, 0).equalsIgnoreCase("[MA]")) { + return; + } + + Location location = event.getBlock().getLocation(); + String arenaId = trim(event, 1); + String signType = trim(event, 2).toLowerCase(); + String templateId = trim(event, 3).toLowerCase(); + + if (templateId.isEmpty()) { + templateId = signType; + } + + Player player = event.getPlayer(); + try { + storesNewSign.store(location, arenaId, templateId, signType); + messenger.tell(player, "New " + signType + " sign created for arena " + arenaId); + + String[] lines = rendersTemplate.render(templateId, arenaId); + IntStream.range(0, 4) + .forEach(i -> event.setLine(i, lines[i])); + } catch (IllegalArgumentException e) { + messenger.tell(player, e.getMessage()); + } + } + + private String trim(SignChangeEvent event, int index) { + String line = event.getLine(index); + if (line == null) { + return ""; + } + return line.trim(); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java new file mode 100644 index 0000000..348869b --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/HandlesSignDestruction.java @@ -0,0 +1,33 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.Messenger; +import org.bukkit.Location; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; + +class HandlesSignDestruction implements Listener { + + private final RemovesSignAtLocation removesSignAtLocation; + private final Messenger messenger; + + HandlesSignDestruction( + RemovesSignAtLocation removesSignAtLocation, + Messenger messenger + ) { + this.removesSignAtLocation = removesSignAtLocation; + this.messenger = messenger; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(BlockBreakEvent event) { + Location location = event.getBlock().getLocation(); + removesSignAtLocation.remove(location) + .ifPresent(sign -> messenger.tell( + event.getPlayer(), + "Removed " + sign.type + " sign for arena " + sign.arenaId + )); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/InvokesSignAction.java b/src/main/java/com/garbagemule/MobArena/signs/InvokesSignAction.java new file mode 100644 index 0000000..e6c89c5 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/InvokesSignAction.java @@ -0,0 +1,50 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.Messenger; +import com.garbagemule.MobArena.Msg; +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import org.bukkit.entity.Player; + +import java.util.function.Consumer; + +class InvokesSignAction { + + private final ArenaMaster arenaMaster; + private final Messenger messenger; + + InvokesSignAction(ArenaMaster arenaMaster, Messenger messenger) { + this.arenaMaster = arenaMaster; + this.messenger = messenger; + } + + void invoke(ArenaSign sign, Player player) { + if (sign.type.equals("join")) { + withArena(sign, player, arena -> { + if (arena.canJoin(player)) { + // Join message is sent in playerJoin + arena.playerJoin(player, player.getLocation()); + } + }); + } else if (sign.type.equals("leave")) { + withArena(sign, player, arena -> { + if (arena.inArena(player) || arena.inLobby(player) || arena.inSpec(player)) { + // Leave message is not sent in playerLeave + if (arena.playerLeave(player)) { + arena.getMessenger().tell(player, Msg.LEAVE_PLAYER_LEFT); + } + } + }); + } + } + + private void withArena(ArenaSign sign, Player player, Consumer action) { + Arena arena = arenaMaster.getArenaWithName(sign.arenaId); + if (arena == null) { + messenger.tell(player, "Arena " + sign.arenaId + " not found"); + return; + } + action.accept(arena); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java b/src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java new file mode 100644 index 0000000..6074094 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/LoadsSignStore.java @@ -0,0 +1,46 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.MobArena; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +class LoadsSignStore { + + private final MobArena plugin; + + LoadsSignStore(MobArena plugin) { + this.plugin = plugin; + } + + SignStore load() { + YamlConfiguration yaml = new YamlConfiguration(); + try { + File data = new File(plugin.getDataFolder(), "data"); + yaml.load(new File(data, SignStore.FILENAME)); + } catch (FileNotFoundException e) { + return new SignStore(Collections.emptyList()); + } catch (InvalidConfigurationException e) { + String msg = SignStore.FILENAME + " is invalid! You may have to delete it."; + throw new IllegalStateException(msg, e); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + List signs = yaml.getList("signs").stream() + .filter(raw -> raw instanceof ArenaSign) + .map(raw -> (ArenaSign) raw) + .collect(Collectors.toList()); + + plugin.getLogger().info("Loaded " + signs.size() + " arena signs."); + + return new SignStore(signs); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java b/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java new file mode 100644 index 0000000..b2c0c9a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/LoadsTemplateStore.java @@ -0,0 +1,106 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.MobArena; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class LoadsTemplateStore { + + private final MobArena plugin; + + LoadsTemplateStore(MobArena plugin) { + this.plugin = plugin; + } + + TemplateStore read() { + YamlConfiguration yaml = new YamlConfiguration(); + try { + File file = new File(plugin.getDataFolder(), TemplateStore.FILENAME); + if (!file.exists()) { + plugin.getLogger().info(TemplateStore.FILENAME + " not found, creating default..."); + plugin.saveResource(TemplateStore.FILENAME, false); + } + yaml.load(file); + } catch (InvalidConfigurationException e) { + throw new IllegalStateException(TemplateStore.FILENAME + " is invalid!", e); + } catch (FileNotFoundException e) { + throw new IllegalStateException(TemplateStore.FILENAME + " is missing!", e); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + Map map = new HashMap<>(); + for (String key : yaml.getKeys(false)) { + validateTemplateNode(key, yaml); + String templateId = stripStateSuffix(key); + map.computeIfAbsent(templateId, id -> loadTemplate(id, yaml)); + } + + plugin.getLogger().info("Loaded " + map.size() + " sign templates."); + + return new TemplateStore(map); + } + + private void validateTemplateNode(String key, YamlConfiguration yaml) { + List list = yaml.getList(key); + if (list == null) { + String msg = "Template " + key + " in " + TemplateStore.FILENAME + " is not a list!"; + throw new IllegalStateException(msg); + } + list.forEach(element -> { + if (!(element instanceof String)) { + String msg = "Template " + key + " in " + TemplateStore.FILENAME + " is not a valid list of strings!"; + throw new IllegalStateException(msg); + } + }); + } + + private String stripStateSuffix(String id) { + if (hasStateSuffix(id)) { + return id.split("-", -1)[0]; + } + return id; + } + + private boolean hasStateSuffix(String id) { + return id.endsWith("-idle") + || id.endsWith("-joining") + || id.endsWith("-running"); + } + + private Template loadTemplate(String id, YamlConfiguration yaml) { + String[] base = getLines(yaml, id); + String[] idle = getLines(yaml, id + "-idle"); + String[] joining = getLines(yaml, id + "-joining"); + String[] running = getLines(yaml, id + "-running"); + + return new Template.Builder(id) + .withBase(base) + .withIdle(idle) + .withJoining(joining) + .withRunning(running) + .build(); + } + + private String[] getLines(YamlConfiguration config, String id) { + List list = config.getStringList(id); + if (list.isEmpty()) { + return null; + } + while (list.size() < 4) { + list.add(""); + } + if (list.size() > 4) { + list = list.subList(0, 4); + } + return list.toArray(new String[0]); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java b/src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java new file mode 100644 index 0000000..3c6856d --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/RedrawsArenaSigns.java @@ -0,0 +1,61 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import org.bukkit.ChatColor; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class RedrawsArenaSigns { + + private final SignStore signStore; + private final TemplateStore templateStore; + private final RendersTemplate rendersTemplate; + private final SetsLines setsSignLines; + + RedrawsArenaSigns( + SignStore signStore, + TemplateStore templateStore, + RendersTemplate rendersTemplate, + SetsLines setsSignLines + ) { + this.signStore = signStore; + this.templateStore = templateStore; + this.rendersTemplate = rendersTemplate; + this.setsSignLines = setsSignLines; + } + + void redraw(Arena arena) { + List signs = signStore.findByArenaId(arena.configName()); + + Map rendered = signs.stream() + .map(sign -> sign.templateId) + .distinct() + .collect(Collectors.toMap( + templateId -> templateId, + templateId -> render(templateId, arena) + )); + + signs.forEach(sign -> setsSignLines.set( + sign.location, + rendered.get(sign.templateId) + )); + } + + private String[] render(String templateId, Arena arena) { + return templateStore.findById(templateId) + .map(template -> rendersTemplate.render(template, arena)) + .orElseGet(() -> notFound(templateId)); + } + + private static String[] notFound(String templateId) { + return new String[]{ + String.join(ChatColor.MAGIC + "b" + ChatColor.RESET, "BROKEN".split("")), + "Template", + ChatColor.BOLD + templateId, + "not found! :(" + }; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java b/src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java new file mode 100644 index 0000000..054de17 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/RedrawsSignsOnUpdates.java @@ -0,0 +1,65 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.MobArena; +import com.garbagemule.MobArena.events.*; +import com.garbagemule.MobArena.framework.Arena; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.scheduler.BukkitScheduler; + +class RedrawsSignsOnUpdates implements Listener { + + private final RedrawsArenaSigns redrawsArenaSigns; + private final BukkitScheduler scheduler; + private final MobArena plugin; + + RedrawsSignsOnUpdates( + RedrawsArenaSigns redrawsArenaSigns, + MobArena plugin + ) { + this.redrawsArenaSigns = redrawsArenaSigns; + this.scheduler = plugin.getServer().getScheduler(); + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(ArenaPlayerJoinEvent event) { + handle(event.getArena()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(ArenaPlayerLeaveEvent event) { + handle(event.getArena()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(ArenaPlayerReadyEvent event) { + handle(event.getArena()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(ArenaStartEvent event) { + handle(event.getArena()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(NewWaveEvent event) { + handle(event.getArena()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(ArenaPlayerDeathEvent event) { + handle(event.getArena()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void on(ArenaEndEvent event) { + handle(event.getArena()); + } + + private void handle(Arena arena) { + scheduler.runTask(plugin, () -> redrawsArenaSigns.redraw(arena)); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java b/src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java new file mode 100644 index 0000000..4bf9c92 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/RemovesSignAtLocation.java @@ -0,0 +1,26 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; + +import java.util.Optional; + +class RemovesSignAtLocation { + + private final SignStore signStore; + private final SavesSignStore savesSignStore; + + RemovesSignAtLocation( + SignStore signStore, + SavesSignStore savesSignStore + ) { + this.signStore = signStore; + this.savesSignStore = savesSignStore; + } + + Optional remove(Location location) { + Optional sign = signStore.remove(location); + sign.ifPresent(s -> savesSignStore.save(signStore)); + return sign; + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java new file mode 100644 index 0000000..48f7188 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplate.java @@ -0,0 +1,76 @@ +package com.garbagemule.MobArena.signs; + +import static java.lang.String.valueOf; + +import com.garbagemule.MobArena.framework.Arena; +import org.bukkit.ChatColor; + +class RendersTemplate { + + String[] render(Template template, Arena arena) { + String[] lines = getTemplateByState(template, arena); + + String[] result = new String[lines.length]; + for (int i = 0; i < lines.length; i++) { + String rendered = render(lines[i], arena); + result[i] = truncate(rendered); + } + return result; + } + + private String[] getTemplateByState(Template template, Arena arena) { + if (arena.isRunning()) { + return template.running; + } + if (arena.getPlayersInLobby().size() > 0) { + return template.joining; + } + return template.idle; + } + + private String render(String line, Arena arena) { + String result = generic(line, arena); + if (arena.isRunning()) { + result = running(result, arena); + } else { + result = joining(result, arena); + } + return ChatColor.translateAlternateColorCodes('&', result); + } + + private String generic(String line, Arena arena) { + return line + .replace("", arena.configName()) + .replace("", valueOf(arena.getMinPlayers())) + .replace("", valueOf(arena.getMaxPlayers())); + } + + private String running(String line, Arena arena) { + return line + .replace("", valueOf(arena.getPlayerCount())) + .replace("", valueOf(arena.getPlayersInArena().size())) + .replace("", valueOf(arena.getPlayerCount() - arena.getPlayersInArena().size())) + .replace("", valueOf(arena.getWaveManager().getWaveNumber())) + .replace("", valueOf(arena.getWaveManager().getFinalWave())) + .replace("", "-") + .replace("", "-"); + } + + private String joining(String line, Arena arena) { + return line + .replace("", valueOf(arena.getPlayersInLobby().size())) + .replace("", valueOf(arena.getPlayersInLobby().size())) + .replace("", "-") + .replace("", "-") + .replace("", valueOf(arena.getPlayersInLobby().size())) + .replace("", valueOf(arena.getReadyPlayersInLobby().size())); + } + + private String truncate(String rendered) { + if (rendered.length() <= 15) { + return rendered; + } + return rendered.substring(0, 15); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java new file mode 100644 index 0000000..f6a405e --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/RendersTemplateById.java @@ -0,0 +1,36 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; + +class RendersTemplateById { + + private final ArenaMaster arenaMaster; + private final TemplateStore templateStore; + private final RendersTemplate rendersTemplate; + + RendersTemplateById( + ArenaMaster arenaMaster, + TemplateStore templateStore, + RendersTemplate rendersTemplate + ) { + this.arenaMaster = arenaMaster; + this.templateStore = templateStore; + this.rendersTemplate = rendersTemplate; + } + + String[] render(String templateId, String arenaId) { + Template template = templateStore.findById(templateId) + .orElseThrow(() -> new IllegalArgumentException( + "Template " + templateId + " not found" + )); + + Arena arena = arenaMaster.getArenaWithName(arenaId); + if (arena == null) { + throw new IllegalStateException("Arena " + arenaId + " not found"); + } + + return rendersTemplate.render(template, arena); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java b/src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java new file mode 100644 index 0000000..5aa2a0a --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SavesSignStore.java @@ -0,0 +1,32 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.MobArena; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +class SavesSignStore { + + private final MobArena plugin; + + SavesSignStore(MobArena plugin) { + this.plugin = plugin; + } + + void save(SignStore signStore) { + YamlConfiguration yaml = new YamlConfiguration(); + List values = new ArrayList<>(signStore.findAll()); + yaml.set("signs", values); + try { + File data = new File(plugin.getDataFolder(), "data"); + yaml.options().header("MobArena Sign Store\n\nPlease DON'T edit this file by hand!\n"); + yaml.save(new File(data, SignStore.FILENAME)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SetsLines.java b/src/main/java/com/garbagemule/MobArena/signs/SetsLines.java new file mode 100644 index 0000000..24a8784 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SetsLines.java @@ -0,0 +1,22 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; + +class SetsLines { + + void set(Location location, String[] lines) { + BlockState state = location.getBlock().getState(); + if (!(state instanceof Sign)) { + return; + } + + Sign sign = (Sign) state; + for (int i = 0; i < lines.length; i++) { + sign.setLine(i, lines[i]); + } + sign.update(); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java b/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java new file mode 100644 index 0000000..e05abb0 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignBootstrap.java @@ -0,0 +1,130 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.MobArena; + +@SuppressWarnings("WeakerAccess") +public class SignBootstrap { + + private final MobArena plugin; + private final SignStore signStore; + private final TemplateStore templateStore; + + private InvokesSignAction invokesSignAction; + private RedrawsArenaSigns redrawsArenaSigns; + private RemovesSignAtLocation removesSignAtLocation; + private RendersTemplate rendersTemplate; + private RendersTemplateById rendersTemplateById; + private SetsLines setsLines; + private StoresNewSign storesNewSign; + private SavesSignStore savesSignStore; + + private SignBootstrap( + MobArena plugin, + SignStore signStore, + TemplateStore templateStore + ) { + this.plugin = plugin; + this.signStore = signStore; + this.templateStore = templateStore; + } + + MobArena getPlugin() { + return plugin; + } + + SignStore getSignStore() { + return signStore; + } + + TemplateStore getTemplateStore() { + return templateStore; + } + + InvokesSignAction getInvokesSignAction() { + if (invokesSignAction == null) { + invokesSignAction = new InvokesSignAction( + plugin.getArenaMaster(), + plugin.getGlobalMessenger() + ); + } + return invokesSignAction; + } + + RedrawsArenaSigns getRedrawsArenaSigns() { + if (redrawsArenaSigns == null) { + redrawsArenaSigns = new RedrawsArenaSigns( + getSignStore(), + getTemplateStore(), + getRendersTemplate(), + getSetsLines() + ); + } + return redrawsArenaSigns; + } + + RemovesSignAtLocation getRemovesSignAtLocation() { + if (removesSignAtLocation == null) { + removesSignAtLocation = new RemovesSignAtLocation( + getSignStore(), + getSavesSignStore() + ); + } + return removesSignAtLocation; + } + + RendersTemplate getRendersTemplate() { + if (rendersTemplate == null) { + rendersTemplate = new RendersTemplate(); + } + return rendersTemplate; + } + + RendersTemplateById getRendersTemplateById() { + if (rendersTemplateById == null) { + rendersTemplateById = new RendersTemplateById( + plugin.getArenaMaster(), + getTemplateStore(), + getRendersTemplate() + ); + } + return rendersTemplateById; + } + + SetsLines getSetsLines() { + if (setsLines == null) { + setsLines = new SetsLines(); + } + return setsLines; + } + + StoresNewSign getStoresNewSign() { + if (storesNewSign == null) { + storesNewSign = new StoresNewSign( + plugin.getArenaMaster(), + getTemplateStore(), + getSignStore(), + getSavesSignStore() + ); + } + return storesNewSign; + } + + SavesSignStore getSavesSignStore() { + if (savesSignStore == null) { + savesSignStore = new SavesSignStore(plugin); + } + return savesSignStore; + } + + public static SignBootstrap create(MobArena plugin) { + TemplateStore templateStore = new LoadsTemplateStore(plugin).read(); + SignStore signStore = new LoadsSignStore(plugin).load(); + + SignBootstrap bootstrap = new SignBootstrap(plugin, signStore, templateStore); + + plugin.getArenaMaster().getArenas() + .forEach(bootstrap.getRedrawsArenaSigns()::redraw); + + return bootstrap; + } +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java b/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java new file mode 100644 index 0000000..b1276db --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignListeners.java @@ -0,0 +1,66 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.MobArena; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; + +import java.util.ArrayList; +import java.util.List; + +public class SignListeners { + + private final List listeners; + + public SignListeners() { + this.listeners = new ArrayList<>(); + } + + public void register(SignBootstrap bootstrap) { + listeners.add(clicks(bootstrap)); + listeners.add(creation(bootstrap)); + listeners.add(destruction(bootstrap)); + listeners.add(redraw(bootstrap)); + + listeners.forEach(listener -> register(listener, bootstrap)); + } + + private HandlesSignClicks clicks(SignBootstrap bootstrap) { + return new HandlesSignClicks( + bootstrap.getSignStore(), + bootstrap.getInvokesSignAction() + ); + } + + private HandlesSignCreation creation(SignBootstrap bootstrap) { + return new HandlesSignCreation( + bootstrap.getStoresNewSign(), + bootstrap.getRendersTemplateById(), + bootstrap.getPlugin().getGlobalMessenger() + ); + } + + private HandlesSignDestruction destruction(SignBootstrap bootstrap) { + return new HandlesSignDestruction( + bootstrap.getRemovesSignAtLocation(), + bootstrap.getPlugin().getGlobalMessenger() + ); + } + + private RedrawsSignsOnUpdates redraw(SignBootstrap bootstrap) { + return new RedrawsSignsOnUpdates( + bootstrap.getRedrawsArenaSigns(), + bootstrap.getPlugin() + ); + } + + private void register(Listener listener, SignBootstrap bootstrap) { + MobArena plugin = bootstrap.getPlugin(); + plugin.getServer().getPluginManager().registerEvents(listener, plugin); + } + + public void unregister() { + listeners.forEach(HandlerList::unregisterAll); + listeners.clear(); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/SignStore.java b/src/main/java/com/garbagemule/MobArena/signs/SignStore.java new file mode 100644 index 0000000..e244783 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/SignStore.java @@ -0,0 +1,49 @@ +package com.garbagemule.MobArena.signs; + +import org.bukkit.Location; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +class SignStore { + + static final String FILENAME = "signs.data"; + + private final Map signs; + + SignStore(List signs) { + this.signs = signs.stream() + .collect(Collectors.toMap( + sign -> sign.location, + sign -> sign + )); + } + + void store(ArenaSign sign) { + signs.put(sign.location, sign); + } + + Optional remove(Location location) { + ArenaSign sign = signs.remove(location); + return Optional.ofNullable(sign); + } + + Collection findAll() { + return signs.values(); + } + + Optional findByLocation(Location location) { + ArenaSign sign = signs.get(location); + return Optional.ofNullable(sign); + } + + List findByArenaId(String arenaId) { + return signs.values().stream() + .filter(sign -> sign.arenaId.equals(arenaId)) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java b/src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java new file mode 100644 index 0000000..ebf9443 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/StoresNewSign.java @@ -0,0 +1,56 @@ +package com.garbagemule.MobArena.signs; + +import com.garbagemule.MobArena.framework.Arena; +import com.garbagemule.MobArena.framework.ArenaMaster; +import org.bukkit.Location; + +class StoresNewSign { + + private final ArenaMaster arenaMaster; + private final TemplateStore templateStore; + private final SignStore signStore; + private final SavesSignStore savesSignStore; + + StoresNewSign( + ArenaMaster arenaMaster, + TemplateStore templateStore, + SignStore signStore, + SavesSignStore savesSignStore + ) { + this.arenaMaster = arenaMaster; + this.templateStore = templateStore; + this.signStore = signStore; + this.savesSignStore = savesSignStore; + } + + void store( + Location location, + String arenaId, + String templateId, + String signType + ) { + Arena arena = arenaMaster.getArenaWithName(arenaId); + if (arena == null) { + throw new IllegalArgumentException("Arena " + arenaId + " not found"); + } + + templateStore.findById(templateId) + .orElseThrow(() -> new IllegalArgumentException( + "Template " + templateId + " not found" + )); + + switch (signType) { + case "info": + case "join": + case "leave": + break; + default: + throw new IllegalArgumentException("Invalid sign type: " + signType); + } + + signStore.store(new ArenaSign(location, templateId, arenaId, signType)); + + savesSignStore.save(signStore); + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/Template.java b/src/main/java/com/garbagemule/MobArena/signs/Template.java new file mode 100644 index 0000000..3d88767 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/Template.java @@ -0,0 +1,78 @@ +package com.garbagemule.MobArena.signs; + +class Template { + + final String[] idle; + final String[] joining; + final String[] running; + + private Template(String[] idle, String[] joining, String[] running) { + this.idle = idle; + this.joining = joining; + this.running = running; + } + + static class Builder { + + private String id; + private String[] base; + private String[] idle; + private String[] joining; + private String[] running; + + Builder(String id) { + this.id = id; + } + + Builder withBase(String[] lines) { + this.base = lines; + return this; + } + + Builder withIdle(String[] lines) { + this.idle = lines; + return this; + } + + Builder withJoining(String[] lines) { + this.joining = lines; + return this; + } + + Builder withRunning(String[] lines) { + this.running = lines; + return this; + } + + Template build() { + if (base == null) { + if (idle == null) { + missing("idle"); + } + if (joining == null) { + missing("joining"); + } + if (running == null) { + missing("running"); + } + } + if (idle == null) { + idle = base; + } + if (joining == null) { + joining = base; + } + if (running == null) { + running = base; + } + return new Template(idle, joining, running); + } + + private void missing(String state) { + String msg = "Missing either base template '" + id + "' or state template '" + id + "-" + state + "'"; + throw new IllegalArgumentException(msg); + } + + } + +} diff --git a/src/main/java/com/garbagemule/MobArena/signs/TemplateStore.java b/src/main/java/com/garbagemule/MobArena/signs/TemplateStore.java new file mode 100644 index 0000000..c18b626 --- /dev/null +++ b/src/main/java/com/garbagemule/MobArena/signs/TemplateStore.java @@ -0,0 +1,21 @@ +package com.garbagemule.MobArena.signs; + +import java.util.Map; +import java.util.Optional; + +class TemplateStore { + + static final String FILENAME = "signs.yml"; + + private final Map templates; + + TemplateStore(Map templates) { + this.templates = templates; + } + + Optional