package tc.oc.pgm.bossbar; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.inject.Inject; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; import org.bukkit.boss.BossBar; import org.bukkit.entity.Player; import org.bukkit.event.EventException; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import tc.oc.commons.bukkit.bossbar.BossBarFactory; import tc.oc.commons.bukkit.chat.ComponentRenderContext; import tc.oc.commons.core.chat.Components; import tc.oc.commons.core.util.MapUtils; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.PlayerChangePartyEvent; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchScope; @ListenerScope(MatchScope.LOADED) public class BossBarMatchModule extends MatchModule implements Listener { // Sources that are automatically rendered for all players, even those // who join after the source is added. The source can still hide itself for // specific players at render time. private final Set<BossBarSource> globalSources = new HashSet<>(); // Views by viewer and source, including global sources private final Table<Player, BossBarSource, View> views = HashBasedTable.create(); @Inject private BossBarFactory bossBarFactory; @Inject private ComponentRenderContext renderer; public void add(BossBarSource source) { if(globalSources.add(source)) { match.players().forEach( player -> views.put(player.getBukkit(), source, new View(source, player.getBukkit())) ); } } public void remove(BossBarSource source) { globalSources.remove(source); final Map<Player, View> playerViews = views.columnMap().remove(source); if(playerViews != null) { playerViews.values().forEach(View::destroy); } } public void add(BossBarSource source, Stream<Player> viewers) { viewers.forEach(viewer -> { views.row(viewer).computeIfAbsent(source, s -> new View(source, viewer)); }); } public void remove(BossBarSource source, Stream<Player> viewers) { viewers.forEach(viewer -> { final View view = views.remove(viewer, source); if(view != null) view.destroy(); }); } public void render(BossBarSource source) { views.column(source).values().forEach(View::render); } public void invalidate(BossBarSource source) { views.column(source).values().forEach(View::invalidate); } public void render(BossBarSource source, Player viewer) { MapUtils.value(views, viewer, source).ifPresent(View::render); } public void invalidate(BossBarSource source, Player viewer) { MapUtils.value(views, viewer, source).ifPresent(View::invalidate); } @EventHandler(priority = EventPriority.MONITOR) public void onJoinLeave(PlayerChangePartyEvent event) throws EventException { final Player viewer = event.getPlayer().getBukkit(); if(event.isLeavingMatch()) { Optional.ofNullable(views.rowMap().remove(viewer)) .ifPresent(row -> row.values().forEach(View::destroy)); } event.yield(); if(event.isJoiningMatch()) { for(BossBarSource source : globalSources) { views.put(viewer, source, new View(source, viewer)); } } } private class View { final BossBarSource source; final Player viewer; final BossBar bar; View(BossBarSource source, Player viewer) { this.source = source; this.viewer = viewer; this.bar = bossBarFactory.createBossBar(Components.blank(), BarColor.WHITE, BarStyle.SOLID); render(); bar.addPlayer(viewer); } void destroy() { bar.removePlayer(viewer); } public void invalidate() { match.getScheduler(MatchScope.LOADED).debounceTask(this::render); } public void render() { final Optional<BossBarContent> content = source.barContent(viewer); if(!content.isPresent()) { bar.setVisible(false); return; // Don't try to get any other properties when hidden } bar.update(renderer.render(content.get().text(), viewer), content.get().progress(), source.barColor(viewer), source.barStyle(viewer), source.barFlags(viewer)); bar.setVisible(true); } } }