package tc.oc.commons.bukkit.tablist; 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.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerSkinPartsChangeEvent; import org.bukkit.plugin.Plugin; import tc.oc.commons.bukkit.nick.PlayerIdentityChangeEvent; import tc.oc.commons.core.logging.ClassLogger; import tc.oc.commons.core.util.DefaultMapAdapter; import tc.oc.commons.core.util.DefaultProvider; import javax.annotation.Nullable; import java.util.Map; import java.util.logging.Logger; /** * Custom player list display (for 1.8 and later) * * Class Overview: * TabManager God object that connects everything together, should only need one of these * TabView A single player's custom tab list * TabEntry Abstract base for a piece of content that goes in a tab list slot * GenericTabEntry Adds a few more generic things to TabEntry * TextTabEntry TabEntry containing arbitrary text content * PlayerTabEntry TabEntry showing a player's name and skin * BlankTabEntry A pool of these is used to fill in empty slots * TabRender Instantiated for each render, contains all the hacky stuff * * A single TabEntry can be part of multiple TabViews simultaneously, and can show different content * for each view. The idea is that TabEntry subclasses can be quite smart and generate their content * dynamically. They have a dirty flag, so the content is not generated unless the TabEntry has been * invalidated. They can also respond to events by invalidating themselves, which is automatically * propagated to any TabViews that contain them. * * Rendering is deferred and always happens through the TabManager.render() method. This will check all * views for dirtiness and render them. It is left to subclasses to call this method, so they can * render whenever they want, potentially deferring it for efficiency. However, all views must be * rendered together. It is not possible to render views individually, because this would make the * TabEntry dirty state very difficult to track. */ public class TabManager implements Listener { protected final Logger logger; protected final Plugin plugin; final DefaultMapAdapter<Player, TabView> enabledViews; final DefaultMapAdapter<Player, TabEntry> playerEntries; final Map<Integer, TabEntry> blankEntries = new DefaultMapAdapter<Integer, TabEntry>(new BlankTabEntry.Factory(), true); boolean dirty; public TabManager(Plugin plugin, @Nullable DefaultProvider<Player, ? extends TabView> viewProvider, @Nullable DefaultProvider<Player, ? extends TabEntry> playerEntryProvider) { if(viewProvider == null) viewProvider = new TabView.Factory(); if(playerEntryProvider == null) playerEntryProvider = new PlayerTabEntry.Factory(); this.logger = new ClassLogger(plugin.getLogger(), getClass()); this.plugin = plugin; this.enabledViews = new DefaultMapAdapter<>(viewProvider, true); this.playerEntries = new DefaultMapAdapter<>(playerEntryProvider, true); } public TabManager(Plugin plugin) { this(plugin, null, null); } public Plugin getPlugin() { return plugin; } public @Nullable TabView getViewOrNull(Player viewer) { return this.enabledViews.getOrNull(viewer); } public @Nullable TabView getView(Player viewer) { return this.enabledViews.get(viewer); } protected void removeView(TabView view) { if(this.enabledViews.remove(view.getViewer()) != null) view.disable(); } public @Nullable TabEntry getPlayerEntryOrNull(Player player) { return this.playerEntries.getOrNull(player); } public TabEntry getPlayerEntry(Player player) { if(!player.willBeOnline()) { throw new IllegalStateException("Tried to get TabEntry for disconnecting player"); } return this.playerEntries.get(player); } public @Nullable TabEntry removePlayerEntry(Player player) { return this.playerEntries.remove(player); } protected TabEntry getBlankEntry(int index) { return this.blankEntries.get(index); } protected void invalidate() { this.dirty = true; } public void render() { if(this.dirty) { for(TabView view : this.enabledViews.values()) { view.render(); } this.dirty = false; } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onQuit(PlayerQuitEvent event) { TabView view = this.getViewOrNull(event.getPlayer()); if(view != null) { view.disable(); this.removeView(view); } this.removePlayerEntry(event.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onRespawn(PlayerRespawnEvent event) { TabView view = this.getViewOrNull(event.getPlayer()); if(view != null) view.onRespawn(event); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onWorldChange(PlayerChangedWorldEvent event) { TabView view = this.getViewOrNull(event.getPlayer()); if(view != null) view.onWorldChange(event); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onNickChange(PlayerIdentityChangeEvent event) { TabEntry entry = this.getPlayerEntryOrNull(event.getPlayer()); if(entry instanceof PlayerTabEntry) { ((PlayerTabEntry) entry).onNickChange(event); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onSkinPartsChange(PlayerSkinPartsChangeEvent event) { TabEntry entry = this.getPlayerEntryOrNull(event.getPlayer()); if(entry instanceof PlayerTabEntry) { ((PlayerTabEntry) entry).onSkinPartsChange(event); } } }