/*
* _____ __ _ _ _____ _
* | __|___ ___ _ _ ___ ___| | |_|___| |_| _ | |_ _ ___
* |__ | -_| _| | | -_| _| |__| |_ -| _| __| | | |_ -|
* |_____|___|_| \_/|___|_| |_____|_|___|_| |__| |_|___|___|
*
* ServerListPlus - http://git.io/slp
* Copyright (c) 2014, Minecrell <https://github.com/Minecrell>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.minecrell.serverlistplus.sponge;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheBuilderSpec;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.inject.Inject;
import net.minecrell.mcstats.SpongeStatsLite;
import net.minecrell.serverlistplus.core.ServerListPlusCore;
import net.minecrell.serverlistplus.core.ServerListPlusException;
import net.minecrell.serverlistplus.core.config.PluginConf;
import net.minecrell.serverlistplus.core.config.storage.InstanceStorage;
import net.minecrell.serverlistplus.core.favicon.FaviconHelper;
import net.minecrell.serverlistplus.core.favicon.FaviconSource;
import net.minecrell.serverlistplus.core.logging.ServerListPlusLogger;
import net.minecrell.serverlistplus.core.player.PlayerIdentity;
import net.minecrell.serverlistplus.core.plugin.ScheduledTask;
import net.minecrell.serverlistplus.core.plugin.ServerListPlusPlugin;
import net.minecrell.serverlistplus.core.plugin.ServerType;
import net.minecrell.serverlistplus.core.status.ResponseFetcher;
import net.minecrell.serverlistplus.core.status.StatusManager;
import net.minecrell.serverlistplus.core.status.StatusRequest;
import net.minecrell.serverlistplus.core.status.StatusResponse;
import net.minecrell.serverlistplus.core.util.Helper;
import net.minecrell.serverlistplus.core.util.Randoms;
import net.minecrell.serverlistplus.sponge.protocol.DummyStatusProtocolHandler;
import net.minecrell.serverlistplus.sponge.protocol.StatusProtocolHandler;
import net.minecrell.serverlistplus.sponge.protocol.StatusProtocolHandlerImpl;
import org.slf4j.Logger;
import org.spongepowered.api.Game;
import org.spongepowered.api.Platform;
import org.spongepowered.api.command.CommandCallable;
import org.spongepowered.api.command.CommandException;
import org.spongepowered.api.command.CommandResult;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.event.game.state.GameStoppingServerEvent;
import org.spongepowered.api.event.network.ClientConnectionEvent;
import org.spongepowered.api.event.server.ClientPingServerEvent;
import org.spongepowered.api.network.status.Favicon;
import org.spongepowered.api.plugin.Dependency;
import org.spongepowered.api.plugin.Plugin;
import org.spongepowered.api.plugin.PluginManager;
import org.spongepowered.api.profile.GameProfile;
import org.spongepowered.api.service.ban.BanService;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.serializer.TextSerializers;
import org.spongepowered.api.util.annotation.NonnullByDefault;
import org.spongepowered.api.world.World;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Plugin(id = "serverlistplus", name = "ServerListPlus",
dependencies = @Dependency(id = "statusprotocol", optional = true))
public class SpongePlugin implements ServerListPlusPlugin {
@Inject protected Game game;
@Inject protected Logger logger;
@ConfigDir(sharedRoot = false) @Inject
protected File configDir;
@Inject
protected SpongeStatsLite stats;
private final StatusProtocolHandler handler;
private ServerListPlusCore core;
private Object loginListener, pingListener;
// Favicon cache
private final CacheLoader<FaviconSource, Optional<Favicon>> faviconLoader =
new CacheLoader<FaviconSource, Optional<Favicon>>() {
@Override
public Optional<Favicon> load(FaviconSource source) throws Exception {
// Try loading the favicon
BufferedImage image = FaviconHelper.loadSafely(core, source);
if (image == null) return Optional.empty(); // Favicon loading failed
else return Optional.of(game.getRegistry().loadFavicon(image)); // Success!
}
};
private LoadingCache<FaviconSource, Optional<Favicon>> faviconCache;
@Inject
public SpongePlugin(PluginManager pluginManager) {
this.handler = pluginManager.isLoaded("statusprotocol") ? new StatusProtocolHandlerImpl() : new DummyStatusProtocolHandler();
}
@Listener
public void enable(GamePreInitializationEvent event) {
if (this.handler.isDummy()) {
this.logger.warn("You don't have StatusProtocol installed. Support for custom player slots will be disabled. Please install it from "
+ "https://github.com/Minecrell/statusprotocol/releases if you intend to use this feature.");
}
try {
this.core = new ServerListPlusCore(this);
logger.info("Successfully loaded!");
} catch (ServerListPlusException e) {
logger.info("Please fix the error before restarting the server!");
return;
} catch (Exception e) {
logger.error("An internal error occurred while loading the core.", e);
return;
}
game.getCommandManager().register(this, new ServerListPlusCommand(), "serverlistplus", "serverlist+",
"serverlist", "slp", "sl+", "s++", "serverping+", "serverping", "spp", "slus");
}
@Listener
public void disable(GameStoppingServerEvent event) {
try {
core.stop();
} catch (ServerListPlusException ignored) {}
}
private static final Pattern ARGUMENT_PATTERN = Pattern.compile(" ", Pattern.LITERAL);
@NonnullByDefault
public final class ServerListPlusCommand implements CommandCallable {
@Override
public CommandResult process(CommandSource source, String arguments) throws CommandException {
String[] args = arguments.isEmpty() ? new String[0] : ARGUMENT_PATTERN.split(arguments);
core.executeCommand(new SpongeCommandSender(source), "serverlistplus", args);
return CommandResult.success();
}
@Override
public List<String> getSuggestions(CommandSource source, String arguments) throws CommandException {
return core.tabComplete(new SpongeCommandSender(source), "serverlistplus",
ARGUMENT_PATTERN.split(arguments));
}
@Override
public boolean testPermission(CommandSource source) {
return true;
}
@Override
public Optional<? extends Text> getShortDescription(CommandSource source) {
return Optional.empty();
}
@Override
public Optional<? extends Text> getHelp(CommandSource source) {
return Optional.empty();
}
@Override
public Text getUsage(CommandSource source) {
return Text.of();
}
}
// Player tracking
public final class LoginListener {
private LoginListener() {}
@Listener
public void onPlayerJoin(ClientConnectionEvent.Login event) {
core.updateClient(event.getConnection().getAddress().getAddress(),
event.getProfile().getUniqueId(), event.getProfile().getName().get());
}
@Listener
public void onPlayerQuit(ClientConnectionEvent.Disconnect event) {
core.updateClient(event.getTargetEntity().getConnection().getAddress().getAddress(),
event.getTargetEntity().getUniqueId(), event.getTargetEntity().getName());
}
}
public final class PingListener {
private PingListener() {}
@Listener
public void onStatusPing(ClientPingServerEvent event) {
StatusRequest request = core.createRequest(event.getClient().getAddress().getAddress());
event.getClient().getVirtualHost().ifPresent(request::setTarget);
handler.getProtocolVersion(event).ifPresent(request::setProtocolVersion);
final ClientPingServerEvent.Response ping = event.getResponse();
final ClientPingServerEvent.Response.Players players = ping.getPlayers().orElse(null);
StatusResponse response = request.createResponse(core.getStatus(), new ResponseFetcher() {
@Override
public Integer getOnlinePlayers() {
return players != null ? players.getOnline() : null;
}
@Override
public Integer getMaxPlayers() {
return players != null ? players.getMax() : null;
}
@Override
public int getProtocolVersion() {
return handler.getProtocolVersion(ping).orElse(-1);
}
});
// Description
String message = response.getDescription();
if (message != null) ping.setDescription(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(message));
// Version
handler.setVersion(ping, response);
// Favicon
FaviconSource favicon = response.getFavicon();
if (favicon != null) {
Optional<Favicon> icon = faviconCache.getUnchecked(favicon);
if (icon.isPresent()) ping.setFavicon(icon.get());
}
if (players != null) {
if (response.hidePlayers()) {
ping.setHidePlayers(true);
} else {
// Online players
Integer count = response.getOnlinePlayers();
if (count != null) players.setOnline(count);
// Max players
count = response.getMaxPlayers();
if (count != null) players.setMax(count);
message = response.getPlayerHover();
if (message != null) {
List<GameProfile> profiles = players.getProfiles();
profiles.clear();
if (response.useMultipleSamples()) {
count = response.getDynamicSamples();
List<String> lines = count != null ? Helper.splitLinesCached(message, count) :
Helper.splitLinesCached(message);
for (String line : lines) {
profiles.add(GameProfile.of(StatusManager.EMPTY_UUID, line));
}
} else
profiles.add(GameProfile.of(StatusManager.EMPTY_UUID, message));
}
}
}
}
}
@Override
public ServerListPlusCore getCore() {
return core;
}
@Override
public ServerType getServerType() {
return ServerType.SPONGE;
}
@Override
public String getServerImplementation() {
Platform platform = game.getPlatform();
return platform.getImplementation().getName() + " v" + platform.getImplementation().getVersion()
+ " (" + platform.getApi().getName() + " v" + platform.getApi().getVersion() + ')';
}
@Override
public Path getPluginFolder() {
return configDir.toPath();
}
@Override
public Integer getOnlinePlayers(String location) {
World world = game.getServer().getWorld(location).orElse(null);
if (world == null) return null;
int count = 0;
for (Player player : game.getServer().getOnlinePlayers()) {
if (player.getWorld().equals(world)) count++;
}
return count;
}
@Override
public Iterator<String> getRandomPlayers() {
Collection<Player> players = game.getServer().getOnlinePlayers();
List<String> result = new ArrayList<>(players.size());
for (Player player : players) {
result.add(player.getName());
}
return Randoms.shuffle(result).iterator();
}
@Override
public Iterator<String> getRandomPlayers(String location) {
World world = game.getServer().getWorld(location).orElse(null);
if (world == null) return null;
Collection<Player> players = game.getServer().getOnlinePlayers();
List<String> result = new ArrayList<>();
for (Player player : players) {
if (player.getWorld().equals(world)) {
result.add(player.getName());
}
}
if (result.isEmpty())
return null;
return Randoms.shuffle(result).iterator();
}
@Override
public Cache<?, ?> getRequestCache() {
return null;
}
@Override
public LoadingCache<FaviconSource, ?> getFaviconCache() {
return faviconCache;
}
@Override
public void runAsync(Runnable task) {
game.getScheduler().createTaskBuilder().async().execute(task).submit(this);
}
@Override
public ScheduledTask scheduleAsync(Runnable task, long repeat, TimeUnit unit) {
return new ScheduledSpongeTask(game.getScheduler().createTaskBuilder()
.async().interval(repeat, unit).execute(task).submit(this));
}
@Override
public String colorize(String s) {
return TextSerializers.FORMATTING_CODE.replaceCodes(s, TextSerializers.LEGACY_FORMATTING_CODE);
}
@Override
public ServerListPlusLogger createLogger(ServerListPlusCore core) {
return new Slf4jServerListPlusLogger(core, logger);
}
@Override
public void initialize(ServerListPlusCore core) {
}
@Override
public void reloadCaches(ServerListPlusCore core) {
}
@Override
public void reloadFaviconCache(CacheBuilderSpec spec) {
if (spec != null) {
this.faviconCache = CacheBuilder.from(spec).build(faviconLoader);
} else {
// Delete favicon cache
faviconCache.invalidateAll();
faviconCache.cleanUp();
this.faviconCache = null;
}
}
@Override
public void configChanged(ServerListPlusCore core, InstanceStorage<Object> confs) {
// Player tracking
if (confs.get(PluginConf.class).PlayerTracking.Enabled) {
if (loginListener == null) {
game.getEventManager().registerListeners(this, this.loginListener = new LoginListener());
logger.debug("Registered player tracking listener.");
}
} else if (loginListener != null) {
game.getEventManager().unregisterListeners(loginListener);
this.loginListener = null;
logger.debug("Unregistered player tracking listener.");
}
// Plugin statistics
if (confs.get(PluginConf.class).Stats) {
this.stats.start();
} else {
this.stats.stop();
}
}
@Override
public void statusChanged(StatusManager status, boolean hasChanges) {
// Status listener
if (hasChanges) {
if (pingListener == null) {
game.getEventManager().registerListeners(this, this.pingListener = new PingListener());
logger.debug("Registered ping listener.");
}
} else if (pingListener != null) {
game.getEventManager().unregisterListeners(pingListener);
this.pingListener = null;
logger.debug("Unregistered ping listener.");
}
}
@Override
public boolean isBanned(PlayerIdentity playerIdentity) {
if (game.getServiceManager().provide(BanService.class).isPresent()) {
final GameProfile profile = GameProfile.of(playerIdentity.getUuid(), playerIdentity.getName());
return game.getServiceManager().provide(BanService.class).get().isBanned(profile);
}
return false;
}
}