/*
* This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT).
*
* Copyright (c) JCThePants (www.jcwhatever.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.jcwhatever.nucleus.internal.actionbar;
import com.jcwhatever.nucleus.Nucleus;
import com.jcwhatever.nucleus.collections.ElementCounter;
import com.jcwhatever.nucleus.collections.ElementCounter.RemovalPolicy;
import com.jcwhatever.nucleus.collections.SetMap;
import com.jcwhatever.nucleus.collections.WeakHashSetMap;
import com.jcwhatever.nucleus.collections.players.PlayerMap;
import com.jcwhatever.nucleus.collections.timed.TimedDistributor;
import com.jcwhatever.nucleus.internal.NucMsg;
import com.jcwhatever.nucleus.managed.actionbar.ActionBarPriority;
import com.jcwhatever.nucleus.managed.scheduler.Scheduler;
import com.jcwhatever.nucleus.utils.ArrayUtils;
import com.jcwhatever.nucleus.utils.TimeScale;
import com.jcwhatever.nucleus.utils.nms.INmsActionBarHandler;
import com.jcwhatever.nucleus.utils.nms.NmsUtils;
import com.jcwhatever.nucleus.utils.text.dynamic.IDynamicText;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Sends action bars to players and manages {@link PersistentActionBar}'s
* per player.
*
* <p>Also manages packet send timing to reduce network traffic and client side lag
* based on the max refresh rate required to persist the bar and the minimum
* refresh rate preferred by the action bars dynamic text. The absolute minimum
* refresh rate is 1 tick while the absolute max is 40 ticks. The refresh rate is
* dynamic, meaning it may change due to the dynamic texts refresh rate being dynamic.
* The refresh rate is managed per each {@link PersistentActionBar} instance.</p>
*/
class BarSender implements Runnable {
static final int MAX_REFRESH_RATE = 10 * 50;
static final int MIN_REFRESH_RATE = 50;
private static final Map<UUID, BarDistributor> PLAYER_MAP = new PlayerMap<>(Nucleus.getPlugin());
private static final SetMap<ActionBar, PlayerBar> BAR_MAP = new WeakHashSetMap<>(35, 3);
private static final INmsActionBarHandler NMS_HANDLER;
static volatile BarSender INSTANCE;
static {
NMS_HANDLER = NmsUtils.getActionBarHandler();
if (NMS_HANDLER == null) {
NucMsg.debug("Failed to get Action Bar NMS handler.");
}
}
/**
* Determine if the player is currently viewing any persistent action bar.
*
* @param player The player to check.
*/
static boolean isViewing(Player player) {
synchronized (PLAYER_MAP) {
return PLAYER_MAP.containsKey(player.getUniqueId());
}
}
/**
* Determine if the player is currently viewing a persistent action bar.
*
* @param player The player to check.
* @param actionBar The action bar to check.
*/
static boolean isViewing(Player player, PersistentActionBar actionBar) {
BarDistributor distributor;
synchronized (PLAYER_MAP) {
distributor = PLAYER_MAP.get(player.getUniqueId());
}
if (distributor == null)
return false;
synchronized (distributor.sync) {
return distributor.contains(
new PlayerBar(player, actionBar, 0, null, ActionBarPriority.DEFAULT));
}
}
/**
* Start the sender. Does nothing if already started.
*/
static void start() {
if (INSTANCE != null || NMS_HANDLER == null)
return;
synchronized (PLAYER_MAP) {
if (INSTANCE != null)
return;
INSTANCE = new BarSender();
Scheduler.runTaskRepeatAsync(Nucleus.getPlugin(), 1, MIN_REFRESH_RATE / 50, INSTANCE);
}
}
/**
* Add a {@link PersistentActionBar} to show to a player.
*
* @param player The player who will see the bar.
* @param actionBar The action bar to show.
* @param duration The duration value. Determines the minimum time slice the bar
* is given when shown with other {@link PersistentActionBar}'s. If the
* action bar is an instance of {@link TimedActionBar}, then duration
* represents the time the bar is displayed before being automatically removed.
* @param timeScale The time scale value.
* @param priority The action bar priority.
*/
static void addBar(Player player, PersistentActionBar actionBar,
int duration, TimeScale timeScale, ActionBarPriority priority) {
if (NMS_HANDLER == null)
return;
PlayerBar playerBar = new PlayerBar(player, actionBar, duration, timeScale, priority);
BarDistributor distributor = BarSender.getDistributor(player);
synchronized (distributor.sync) {
// ensure distributor does not already contain the playerBar
if (distributor.contains(playerBar))
return;
// add playerBar to distributor
distributor.add(playerBar, duration, timeScale);
}
synchronized (PLAYER_MAP) {
// add to BAR_MAP
BAR_MAP.put(actionBar, playerBar);
}
if(INSTANCE == null && Bukkit.isPrimaryThread()) {
start();
}
}
/**
* Remove a {@link PersistentActionBar} from a player view.
*
* @param player The player.
* @param actionBar The action bar to remove.
*/
static void removeBar(Player player, PersistentActionBar actionBar) {
if (NMS_HANDLER == null)
return;
PlayerBar playerBar = new PlayerBar(player, actionBar, 0,
TimeScale.TICKS, ActionBarPriority.DEFAULT);
BarDistributor distributor = BarSender.getDistributor(player);
boolean isEmpty;
synchronized (distributor.sync) {
distributor.remove(playerBar);
isEmpty = distributor.isEmpty();
}
synchronized (PLAYER_MAP) {
if (isEmpty) {
PLAYER_MAP.remove(player.getUniqueId());
}
BAR_MAP.removeValue(actionBar, playerBar);
}
}
/**
* Remove a {@link PersistentActionBar} from a player view.
*
* @param actionBar The action bar to remove.
*/
static void removeBar(PersistentActionBar actionBar) {
if (NMS_HANDLER == null)
return;
Set<PlayerBar> playerBars;
synchronized (PLAYER_MAP) {
playerBars = BAR_MAP.removeAll(actionBar);
}
for (PlayerBar bar : playerBars) {
removeBar(bar);
}
}
static <T extends Collection<Player>> T getViewers(PersistentActionBar actionBar, T output) {
if (NMS_HANDLER == null)
return output;
Set<PlayerBar> playerBars;
synchronized (PLAYER_MAP) {
playerBars = BAR_MAP.getAll(actionBar);
}
if (output instanceof ArrayList)
((ArrayList) output).ensureCapacity(playerBars.size());
for (PlayerBar bar : playerBars) {
output.add(bar.player());
}
return output;
}
/**
* Remove a {@link PlayerBar}.
*
* @param bar The player bar view instance to remove.
*/
static void removeBar(PlayerBar bar) {
if (NMS_HANDLER == null)
return;
BarDistributor distributor = BarSender.getDistributor(bar.player());
boolean isEmpty;
synchronized (distributor.sync) {
distributor.remove(bar);
isEmpty = distributor.isEmpty();
}
if (isEmpty) {
synchronized (PLAYER_MAP) {
PLAYER_MAP.remove(bar.player().getUniqueId());
}
}
}
/**
* Remove all action bars from a player.
*
* @param player The player to remove the action bars from.
*/
static void removePlayer(Player player) {
if (NMS_HANDLER == null)
return;
BarDistributor distributor = PLAYER_MAP.get(player.getUniqueId());
if (distributor == null)
return;
List<PlayerBar> bars = new ArrayList<>(distributor);
for (PlayerBar bar : bars) {
removeBar(player, bar.bar());
}
}
/**
* Get the players current distributor or create a new one.
*
* @param player The player.
*/
static BarDistributor getDistributor(Player player) {
BarDistributor distributor;
synchronized (PLAYER_MAP) {
distributor = PLAYER_MAP.get(player.getUniqueId());
}
if (distributor == null) {
synchronized (PLAYER_MAP) {
distributor = new BarDistributor();
PLAYER_MAP.put(player.getUniqueId(), distributor);
}
}
return distributor;
}
/**
* Get the current priority of the action bar being displayed to
* a player, if any.
*
* @param player The player.
*
* @return The priority or {@link ActionBarPriority#LOW} if no action bars
* are being displayed.
*/
static ActionBarPriority getPriority(Player player) {
BarDistributor distributor = getDistributor(player);
if (distributor == null)
return ActionBarPriority.LOW;
synchronized (distributor.sync) {
return distributor.getHighestPriority();
}
}
@Override
public void run() {
List<BarDistributor> distributors = new ArrayList<>(10);
synchronized (PLAYER_MAP){
if (PLAYER_MAP.isEmpty())
return;
// copy distributors to prevent concurrent modification errors.
distributors.addAll(PLAYER_MAP.values());
}
long now = System.currentTimeMillis();
final List<PlayerBar> toSend = new ArrayList<>(distributors.size() * 2);
for (BarDistributor distributor : distributors) {
PlayerBar playerBar;
synchronized (distributor.sync) {
// get the current action bar
playerBar = distributor.current();
if (playerBar == null)
continue;
while (!distributor.isHighestPriority(playerBar.priority())) {
playerBar = distributor.next();
if (playerBar == null)
throw new AssertionError("Null player bar in bar distributor.");
}
}
// remove expired action bars
if (playerBar.expires() > 0 && playerBar.expires() <= now) {
removeBar(playerBar);
NucMsg.debug("Removing Bar");
continue;
}
// send action bar packet if time to update
if (playerBar.nextUpdate() == 0 ||
playerBar.nextUpdate() <= System.currentTimeMillis()) {
toSend.add(playerBar);
}
}
if (toSend.isEmpty())
return;
Scheduler.runTaskSync(Nucleus.getPlugin(), new Runnable() {
@Override
public void run() {
for (PlayerBar bar : toSend) {
bar.send();
}
}
});
}
/**
* Send an action bar packet to a player.
*
* @param player The player.
* @param actionBar The action bar to send.
*
* @return The next update time in milliseconds.
*/
static long send(final Player player, ActionBar actionBar) {
if (NMS_HANDLER == null)
return 0;
IDynamicText dynText = actionBar.getText();
final CharSequence text = dynText.nextText();
if (text != null) {
NMS_HANDLER.send(ArrayUtils.asList(player), text);
}
int interval = dynText.getRefreshRate();
if (interval <= 0) {
interval = MAX_REFRESH_RATE;
}
else {
interval = Math.min(interval * 50, MAX_REFRESH_RATE);
interval = Math.max(interval, MIN_REFRESH_RATE);
}
return System.currentTimeMillis() + interval;
}
static class BarDistributor extends TimedDistributor<PlayerBar> {
final Object sync = new Object();
final ElementCounter<ActionBarPriority> priority =
new ElementCounter<ActionBarPriority>(RemovalPolicy.REMOVE);
@Override
public boolean add(@Nonnull PlayerBar element, int timeSpan, TimeScale timeScale) {
if (super.add(element, timeSpan, timeScale)) {
priority.add(element.priority());
return true;
}
return false;
}
public boolean remove(PlayerBar bar) {
if (super.remove(bar)) {
priority.subtract(bar.priority());
return true;
}
return false;
}
/**
* Determine if the specified priority is the highest priority.
*
* @param barPriority The priority to check.
*/
public boolean isHighestPriority(ActionBarPriority barPriority) {
switch (barPriority) {
case HIGH:
return true;
case DEFAULT:
return !priority.contains(ActionBarPriority.HIGH);
case LOW:
return !priority.contains(ActionBarPriority.HIGH) &&
!priority.contains(ActionBarPriority.DEFAULT);
default:
throw new AssertionError("Unknown ActionBarPriority enum constant: " + barPriority.name());
}
}
public ActionBarPriority getHighestPriority() {
if (priority.contains(ActionBarPriority.HIGH))
return ActionBarPriority.HIGH;
if (priority.contains(ActionBarPriority.DEFAULT))
return ActionBarPriority.DEFAULT;
return ActionBarPriority.LOW;
}
}
}