package tc.oc.commons.bukkit.teleport; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TranslatableComponent; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import tc.oc.api.docs.Arena; import tc.oc.api.docs.Game; import tc.oc.api.docs.Server; import tc.oc.api.docs.virtual.ServerDoc; import tc.oc.api.games.GameStore; import tc.oc.commons.bukkit.chat.ComponentRenderContext; import tc.oc.commons.bukkit.config.ExternalConfiguration; import tc.oc.commons.bukkit.configuration.ConfigUtils; import tc.oc.commons.bukkit.event.ObserverKitApplyEvent; import tc.oc.commons.bukkit.format.GameFormatter; import tc.oc.commons.bukkit.format.ServerFormatter; import tc.oc.commons.bukkit.inventory.Slot; import tc.oc.commons.bukkit.item.ItemConfigurationParser; import tc.oc.commons.bukkit.item.RenderedItemBuilder; import tc.oc.commons.bukkit.listeners.ButtonListener; import tc.oc.commons.bukkit.listeners.ButtonManager; import tc.oc.commons.bukkit.listeners.WindowListener; import tc.oc.commons.bukkit.listeners.WindowManager; import tc.oc.commons.bukkit.ticket.TicketBooth; import tc.oc.commons.core.chat.Component; import tc.oc.commons.core.chat.Components; import tc.oc.commons.core.inject.InnerFactory; import tc.oc.commons.core.plugin.PluginFacet; import tc.oc.minecraft.api.configuration.InvalidConfigurationException; import static tc.oc.commons.core.exception.LambdaExceptionUtils.rethrowFunction; @Singleton public class NavigatorInterface implements PluginFacet, Listener { private final GameStore games; private final ServerFormatter serverFormatter = ServerFormatter.light; private final GameFormatter gameFormatter; private final TicketBooth ticketBooth; private final ButtonManager buttonManager; private final WindowManager windowManager; private final RenderedItemBuilder.Factory itemBuilders; private final ComponentRenderContext renderer; private final Server localServer; private final Navigator navigator; private boolean enabled; private int height; private BaseComponent title = new TranslatableComponent("navigator.title"); private ItemStack openButtonIcon = new ItemStack(Material.SIGN); private Slot.Player openButtonSlot = Slot.Hotbar.forPosition(0); private ImmutableMap<Slot.Container, Button> buttons = ImmutableMap.of(); private final Set<InventoryView> openWindows = new HashSet<>(); @Inject NavigatorInterface(GameStore games, GameFormatter gameFormatter, TicketBooth ticketBooth, ButtonManager buttonManager, RenderedItemBuilder.Factory itemBuilders, WindowManager windowManager, ComponentRenderContext renderer, Server localServer, Navigator navigator, InnerFactory<NavigatorInterface, Configuration> configFactory) { this.games = games; this.gameFormatter = gameFormatter; this.ticketBooth = ticketBooth; this.buttonManager = buttonManager; this.itemBuilders = itemBuilders; this.windowManager = windowManager; this.renderer = renderer; this.localServer = localServer; this.navigator = navigator; configFactory.create(this); } public void setOpenButtonSlot(Slot.Player openButtonSlot) { this.openButtonSlot = openButtonSlot; } private final ButtonListener openButtonListener = (button, clicker, clickType, event) -> { if(clickType == ClickType.RIGHT) { openWindow(clicker); return true; } return false; }; private ItemStack createOpenButton(Player player) { return buttonManager.createButton(openButtonListener, itemBuilders.create(player, openButtonIcon) .flags(ItemFlag.values()) .name(new Component(title, ChatColor.AQUA, ChatColor.BOLD)) .get()); } public void giveOpenButton(Player player) { openButtonSlot.putItem(player, createOpenButton(player)); } @EventHandler public void onObserve(ObserverKitApplyEvent event) { if(enabled) { giveOpenButton(event.getPlayer()); } } private final WindowListener windowListener = new WindowListener() { @Override public void windowOpened(InventoryView window) { openWindows.add(window); } @Override public void windowClosed(InventoryView window) { openWindows.remove(window); } @Override public boolean windowClicked(InventoryView window, Inventory inventory, ClickType clickType, InventoryType.SlotType slotType, int slotIndex, @Nullable ItemStack item) { return true; } }; private Inventory createWindow(Player player) { final Inventory inventory = Bukkit.createInventory( player, height * 9, renderer.renderLegacy(new Component(title, ChatColor.DARK_AQUA, ChatColor.BOLD), player) ); buttons.values().forEach(handler -> handler.updateWindow(player, inventory)); return inventory; } private void openWindow(Player player) { if(enabled && !buttons.isEmpty()) { windowManager.openWindow(windowListener, player, createWindow(player)); } } private void closeAllWindows() { ImmutableList.copyOf(openWindows).forEach(InventoryView::close); openWindows.clear(); } private void clear() { closeAllWindows(); NavigatorInterface.this.enabled = false; NavigatorInterface.this.buttons.values().forEach(Button::release); NavigatorInterface.this.buttons = ImmutableMap.of(); } class Configuration extends ExternalConfiguration { @Inject public Configuration() {} @Override protected String configName() { return "navigator"; } @Override protected String fileName() { return "navigator-" + localServer.datacenter(); } @Override protected void configChanged(@Nullable ConfigurationSection before, @Nullable ConfigurationSection after) throws InvalidConfigurationException { super.configChanged(before, after); if(after != null) { load(after); } else { clear(); } } void load(ConfigurationSection config) throws InvalidConfigurationException { final boolean enabled; final BaseComponent title; final ItemStack openButtonIcon; final Map<Slot.Container, Button> buttons = new HashMap<>(); try { enabled = config.getBoolean("enabled", false); title = new TranslatableComponent(config.getString("title", "navigator.title")); final ItemConfigurationParser itemParser = new ItemConfigurationParser(config); openButtonIcon = itemParser.getItem(config, "icon", () -> new ItemStack(Material.SIGN)); if(enabled) { final ConfigurationSection buttonSection = config.getSection("buttons"); for(String key : buttonSection.getKeys()) { final Button button = new Button(buttonSection.getSection(key), itemParser); buttons.put(button.slot, button); } } } catch(InvalidConfigurationException e) { buttons.values().forEach(Button::release); throw e; } clear(); NavigatorInterface.this.enabled = enabled; NavigatorInterface.this.title = title; NavigatorInterface.this.openButtonIcon = openButtonIcon; NavigatorInterface.this.buttons = ImmutableMap.copyOf(buttons); NavigatorInterface.this.height = buttons.values() .stream() .mapToInt(button -> button.slot.getRow() + 1) .max() .orElse(0); } } private class Button implements ButtonListener { final Slot.Container slot; final ItemStack icon; final Navigator.Connector connector; final Consumer<Navigator.Connector> observer = c -> openWindows.forEach(window -> updateWindow((Player) window.getPlayer(), window.getTopInventory())); Button(ConfigurationSection config, ItemConfigurationParser itemParser) throws InvalidConfigurationException { this.slot = itemParser.needSlotByPosition(config, null, null, Slot.Container.class); this.icon = config.isString("skull") ? itemParser.needSkull(config, "skull") : itemParser.needItem(config, "icon"); this.connector = navigator.combineConnectors(ConfigUtils.needValueOrList(config, "to", String.class).stream() .map(rethrowFunction(token -> parseConnector(config, "to", token))) .collect(Collectors.toList())); this.connector.startObserving(observer); } private Navigator.Connector parseConnector(ConfigurationSection section, String key, String token) throws InvalidConfigurationException { final Navigator.Connector connector = navigator.parseConnector(token); if(connector == null) { throw new InvalidConfigurationException(section, key, "Invalid connector token '" + token + "'"); } return connector; } void release() { buttonManager.unregisterListener(this); connector.stopObserving(observer); connector.release(); } @Override public boolean buttonClicked(ItemStack stack, Player clicker, ClickType clickType, Event event) { if(connector.isConnectable()) { windowManager.closeWindow(clicker); connector.teleport(clicker); } return true; } void updateWindow(Player viewer, Inventory inventory) { final ItemStack stack = createButton(viewer); if(!Objects.equals(stack, slot.getItem(inventory))) { slot.putItem(inventory, stack); } } @Nullable ItemStack createButton(Player viewer) { final ItemStack icon = createIcon(viewer); return icon == null ? null : buttonManager.createButton(this, icon); } @Nullable ItemStack createIcon(Player viewer) { if(!connector.isVisible()) return null; final RenderedItemBuilder<?> icon = itemBuilders.create(viewer, this.icon.clone()) .flags(ItemFlag.values()); final Object mapped = connector.mappedTo(); if(Navigator.DEFAULT_MAPPING.equals(mapped)) { renderDefault(viewer, icon); } else if(mapped instanceof Server) { renderServer(viewer, icon, (Server) mapped); } else if(mapped instanceof Arena) { renderArena(viewer, icon, (Arena) mapped); } return icon.get(); } void renderDefault(Player viewer, RenderedItemBuilder<?> icon) { final Game game = ticketBooth.currentGame(viewer); if(game != null) { icon.name(gameFormatter.leave(game)); } else { icon.name(new Component(new TranslatableComponent("servers.backToLobby"), ChatColor.AQUA)); } } void renderServer(Player viewer, RenderedItemBuilder<?> icon, Server server) { icon.name(serverFormatter.name(server)); serverFormatter.description(server).ifPresent(icon::lore); icon.lore(Components.blank()); if(serverFormatter.isRestarting(server)) { icon.lore(serverFormatter.onlineStatus(server)); } else { icon.amount(Math.max(1, server.num_online())); if(server.role() == ServerDoc.Role.LOBBY) { icon.lore(gameFormatter.onlineCount(server)); } else { icon.lore(gameFormatter.playingCount(server)); icon.lore(gameFormatter.watchingCount(server)); icon.lore(Components.blank()); if(server.current_match() != null) { if(server.current_match().map() != null && server.current_match().end() == null) { // Show current map if match is not finished icon.lore(serverFormatter.currentMap(server)); } if(server.num_online() > 0 && server.restart_queued_at() == null && (server.current_match().start() == null || server.current_match().end() != null)) { // Enchant if server has players, and is not restarting, and is between matches icon.enchant(Enchantment.ARROW_INFINITE, 1); } } if(server.next_map() != null) { icon.lore(serverFormatter.nextMap(server)); } } } } void renderArena(Player viewer, RenderedItemBuilder<?> icon, Arena arena) { final Game game = games.byId(arena.game_id()); icon.amount(Math.max(1, arena.num_playing() + arena.num_queued())) .name(new Component(game.name(), ChatColor.AQUA, ChatColor.BOLD)) .lore(gameFormatter.description(game)) .lore(Components.blank()) .lore(gameFormatter.playingCount(arena)) .lore(gameFormatter.waitingCount(arena)) .get(); } } }