package com.supaham.commons.bukkit;
import com.google.common.base.Preconditions;
import com.supaham.commons.bukkit.modules.CommonModule;
import com.supaham.commons.bukkit.modules.ModuleContainer;
import com.supaham.commons.bukkit.players.Freeze;
import com.supaham.commons.bukkit.players.Players;
import com.supaham.commons.bukkit.players.Players.PlayersSupplier;
import com.supaham.commons.bukkit.potion.PotionEffectManager;
import com.supaham.commons.bukkit.potion.Potions;
import com.supaham.commons.bukkit.utils.EventUtils;
import com.supaham.commons.state.State;
import com.supaham.commons.utils.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent.Result;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.server.ServerCommandEvent;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Server shutdown task with a {@link DelayedIterator} of all online players that kicks them with
* a nice message.
*
* @see #ServerShutdown(ModuleContainer)
* @see #run()
* @since 0.2
*/
public class ServerShutdown extends CommonModule implements Runnable {
private final DelayedIterator<Player> delayedIterator;
private final PotionEffectManager potionEffectManager;
private final Freeze freeze;
private final Listener listener = new ServerShutdownListener();
private TickerTask rerunTask; // privately used to retry to shutdown if cancelled by preRun.
private String kickMessage;
private String shutdownMessage;
private String restartMessage;
private int shutdownDelay = 0;
private boolean kickingPlayersOnShutdown = true;
private String stopPermission;
private String restartPermission;
private boolean restarting;
/**
* Constructs a new {@link ServerShutdown} in charge of slowly kicking players and stopping the
* server. This is equivalent to calling {@link #ServerShutdown(ModuleContainer, int)} with the
* {@code int} as 5.
*
* @param container module container to own this module
*
* @see #run()
*/
public ServerShutdown(@Nonnull ModuleContainer container) {
this(container, 5);
}
/**
* Constructs a new {@link ServerShutdown} in charge of slowly kicking players and stopping the
* server. This is equivalent to calling {@link #ServerShutdown(ModuleContainer, int,
* PlayersSupplier)} with the PlayersSupplier as null, which then defaults to {@link
* Players#serverPlayers()}.
*
* @param container module container to own this module
* @param interval interval between each player kick
*
* @see #run()
*/
public ServerShutdown(@Nonnull ModuleContainer container, int interval) {
this(container, interval, null);
}
/**
* Constructs a new {@link ServerShutdown} in charge of slowly kicking players and stopping the
* server. If the given {@link PlayersSupplier} is null, it defaults to {@link
* Players#serverPlayers()}.
*
* @param container module container to own this module
* @param interval interval between each player kick
* @param supplier supplier of players to kick
*
* @see #run()
*/
public ServerShutdown(@Nonnull ModuleContainer container, int interval,
@Nullable PlayersSupplier supplier) {
super(container);
ModuleContainer cont = plugin.getModuleContainer();
PotionEffectManager pem = cont.getModule(PotionEffectManager.class);
this.potionEffectManager = pem == null ? new PotionEffectManager(container) : pem;
if (pem == null) { // we created our own potion effect manager.
this.potionEffectManager.setState(State.ACTIVE);
}
Freeze freeze = cont.getModule(Freeze.class);
this.freeze = freeze == null ? new Freeze(container) : freeze;
if (freeze == null) { // we created our own freeze.
this.freeze.setState(State.ACTIVE);
}
if (supplier == null) {
supplier = Players.serverPlayers();
}
this.delayedIterator = new DelayedIterator<Player>(plugin, supplier, interval) {
@Override
public void onRun(Player player) {
if (kickingPlayersOnShutdown) {
player.kickPlayer(ServerShutdown.this.kickMessage);
}
}
@Override
public void onDone() {
new TickerTask(plugin, shutdownDelay, 0) {
@Override public void run() {
if (restarting) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "restart");
} else {
Bukkit.getServer().shutdown();
}
stop();
}
}.start();
}
};
this.rerunTask = new TickerTask(plugin, 0, 1) {
@Override
public void run() {
if (!ServerShutdown.this.isInProgress()) { // This should always be true!
ServerShutdown.this.run();
} else { // this task is still running for some reason. Better safe than sorry.
stop();
}
}
};
registerListener(this.listener);
setKickMessage(null);
}
/**
* Used to start a kicking process followed by server shutdown assuming {@link #preRun()} returns
* true, if it does not, the task retries one tick later.
*/
@Override
public void run() {
run(false);
}
public void run(boolean restart) {
Preconditions.checkState(!isInProgress(), "Shutdown already in progress.");
this.restarting = restart;
if (!preRun()) {
this.state = State.STOPPED;
this.rerunTask.start();
return;
} else {
this.rerunTask.stop();
}
this.state = State.ACTIVE;
this.delayedIterator.start();
if (this.restarting) {
if (this.restartMessage != null) {
this.plugin.getServer().broadcastMessage(this.restartMessage);
}
} else {
if (this.shutdownMessage != null) {
this.plugin.getServer().broadcastMessage(this.shutdownMessage);
}
}
}
@Override public boolean setState(@Nonnull State state) throws UnsupportedOperationException {
boolean change = super.setState(state);
if (change && !state.equals(State.ACTIVE)) { // rerun cannot be registered because it is
// manually activated.
this.delayedIterator.setState(state);
this.rerunTask.setState(state);
}
return change;
}
/**
* This method is executed right before the kicking process begins. This method applies
* {@link Potions#noJump()} and {@link Potions#noWalk()} to players. This method also fires the
* {@link ServerShutdownEvent}.
*/
protected boolean preRun() {
for (Player player : this.delayedIterator.getSupplier().get()) {
this.freeze.freeze(player, -1, true);
}
return !EventUtils.callEvent(new ServerShutdownEvent(this)).isCancelled();
}
/**
* Returns whether this task is in the progress of shutting down the server.
*
* @return whether this task is in progress
*/
public boolean isInProgress() {
return this.delayedIterator.isStarted();
}
/**
* Returns the kick message. If this message was never set, it defaults to {@link
* Server#getShutdownMessage()}.
*
* @return kick message
*/
@Nonnull
public String getKickMessage() {
return kickMessage;
}
/**
* Sets the kick message to send to players, attempting to join, or when kicking them. The
* message can never be null or empty, if that is passed to this method, it sets the message to
* {@link Server#getShutdownMessage()}.
*
* @param kickMessage kick message to set
*/
public void setKickMessage(@Nullable String kickMessage) {
this.kickMessage = StringUtils.stripToNull(kickMessage) == null ? Bukkit.getShutdownMessage()
: kickMessage;
}
/**
* Returns the message to broadcast when this shutdown task begins.
*
* @return broadcast message, nullable but never empty
*/
@Nullable
public String getShutdownMessage() {
return shutdownMessage;
}
/**
* Sets the message to broadcast when this shutdown task begins. If the message is empty it will
* be set to null.
*
* @param shutdownMessage shutdown message to set, nullable
*/
public void setShutdownMessage(@Nullable String shutdownMessage) {
this.shutdownMessage = StringUtils.stripToNull(shutdownMessage);
}
/**
* Returns the message to broadcast when this shutdown task begins.
*
* @return broadcast message, nullable but never empty
*/
@Nullable
public String getRestartMessage() {
return restartMessage;
}
/**
* Sets the message to broadcast when this restart task begins. If the message is empty it will
* be set to null.
*
* @param restartMessage restart message to set, nullable
*/
public void setRestartMessage(@Nullable String restartMessage) {
this.restartMessage = StringUtils.stripToNull(restartMessage);
}
public int getShutdownDelay() {
return shutdownDelay;
}
public void setShutdownDelay(int shutdownDelay) {
this.shutdownDelay = shutdownDelay;
}
public boolean isKickingPlayersOnShutdown() {
return kickingPlayersOnShutdown;
}
public void setKickingPlayersOnShutdown(boolean kickingPlayersOnShutdown) {
this.kickingPlayersOnShutdown = kickingPlayersOnShutdown;
}
public String getStopPermission() {
return stopPermission;
}
public void setStopPermission(String stopPermission) {
this.stopPermission = stopPermission;
}
public String getRestartPermission() {
return restartPermission;
}
public void setRestartPermission(String restartPermission) {
this.restartPermission = restartPermission;
}
private final class ServerShutdownListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onServerCommandEvent(ServerCommandEvent event) {
String command = event.getCommand();
boolean restart = command.startsWith("restart");
if (restart || command.startsWith("stop")) {
ServerShutdown.this.run(restart);
event.setCommand(""); // dont execute stop!
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onServerCommandEvent(PlayerCommandPreprocessEvent event) {
String message = event.getMessage();
if (message.startsWith("/restart")) {
if (!event.getPlayer().hasPermission(getRestartPermission())) {
return;
}
ServerShutdown.this.run(true);
event.setCancelled(true); // dont execute stop!
} else if (message.startsWith("/stop")) {
if (!event.getPlayer().hasPermission(getStopPermission())) {
return;
}
ServerShutdown.this.run(false);
event.setCancelled(true); // dont execute stop!
}
}
@EventHandler
public void onPlayerLogin(AsyncPlayerPreLoginEvent event) {
if (isInProgress()) {
event.disallow(Result.KICK_OTHER, ServerShutdown.this.kickMessage);
}
}
// If a player happened to get to the joining stage when the server is shutting down after login.
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if (isInProgress()) {
event.getPlayer().kickPlayer(ServerShutdown.this.kickMessage);
}
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (isInProgress()) {
event.setCancelled(true);
}
}
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) {
if (isInProgress()) {
event.setCancelled(true);
}
}
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (isInProgress()) {
event.setCancelled(true);
}
}
}
/**
* Called when the server is about to shutdown.
*/
public static class ServerShutdownEvent extends Event implements Cancellable {
private final ServerShutdown serverShutdown;
private boolean cancelled;
public ServerShutdownEvent(@Nonnull ServerShutdown serverShutdown) {
this.serverShutdown = Preconditions.checkNotNull(serverShutdown,
"serverShutdown cannot be null.");
}
public ServerShutdown getServerShutdown() {
return serverShutdown;
}
@Override public boolean isCancelled() {
return cancelled;
}
@Override public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
private static final HandlerList handlerList = new HandlerList();
@Override
public HandlerList getHandlers() { return handlerList; }
public static HandlerList getHandlerList() { return handlerList; }
}
}