/* * _____ __ _ _ _____ _ * | __|___ ___ _ _ ___ ___| | |_|___| |_| _ | |_ _ ___ * |__ | -_| _| | | -_| _| |__| |_ -| _| __| | | |_ -| * |_____|___|_| \_/|___|_| |_____|_|___|_| |__| |_|___|___| * * 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.canary; import com.google.common.base.Optional; 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.mojang.authlib.GameProfile; import lombok.SneakyThrows; import net.canarymod.Canary; import net.canarymod.api.entity.living.humanoid.Player; import net.canarymod.api.world.World; import net.canarymod.chat.ChatFormat; import net.canarymod.chat.MessageReceiver; import net.canarymod.commandsys.Command; import net.canarymod.commandsys.CommandDependencyException; import net.canarymod.commandsys.CommandListener; import net.canarymod.hook.HookHandler; import net.canarymod.hook.player.DisconnectionHook; import net.canarymod.hook.player.PreConnectionHook; import net.canarymod.hook.system.ServerListPingHook; import net.canarymod.plugin.Plugin; import net.canarymod.plugin.PluginListener; 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.visualillusionsent.utils.TaskManager; import org.mcstats.MetricsLite; import java.awt.image.BufferedImage; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; public class CanaryPlugin extends Plugin implements ServerListPlusPlugin { private ServerListPlusCore core; private Path pluginFolder; private PluginListener loginListener, pingListener; private MetricsLite metrics; private final Field PROFILES_FIELD; // Favicon cache private final CacheLoader<FaviconSource, Optional<String>> faviconLoader = new CacheLoader<FaviconSource, Optional<String>>() { @Override public Optional<String> load(FaviconSource source) throws Exception { // Try loading the favicon BufferedImage image = FaviconHelper.loadSafely(core, source); if (image == null) return Optional.absent(); // Favicon loading failed else return Optional.of(CanaryFavicon.create(image)); // Success! } }; private LoadingCache<FaviconSource, Optional<String>> faviconCache; @SneakyThrows public CanaryPlugin() { SnakeYAML.load(); this.PROFILES_FIELD = ServerListPingHook.class.getDeclaredField("profiles"); PROFILES_FIELD.setAccessible(true); } @Override public boolean enable() { this.pluginFolder = Paths.get(Canary.getWorkingPath(), "config", getName()); try { this.core = new ServerListPlusCore(this); getLogman().info("Successfully loaded!"); } catch (ServerListPlusException e) { getLogman().info("Please fix the error before restarting the server!"); return false; } catch (Exception e) { getLogman().error("An internal error occurred while loading the core.", e); return false; } // Register command try { registerCommands(new ServerListPlusCommand(), false); } catch (CommandDependencyException e) { getLogman().error("Failed to register command", e); return false; } return true; } @Override public void disable() { try { core.stop(); } catch (ServerListPlusException ignored) {} } public final class ServerListPlusCommand implements CommandListener { private ServerListPlusCommand() {} @Command(aliases = {"serverlistplus", "serverlist+", "serverlist", "slp", "sl+", "s++", "serverping+", "serverping", "spp", "slus"}, permissions = "", description = "ServerListPlus", toolTip = "") public void onCommand(MessageReceiver sender, String[] args) { core.executeCommand(new CanaryCommandSender(sender), args[0], Arrays.copyOfRange(args, 1, args.length)); } } // Player tracking public final class LoginListener implements PluginListener { private LoginListener() {} @HookHandler public void onPreConnect(PreConnectionHook hook) throws UnknownHostException { core.updateClient(InetAddress.getByName(hook.getIp()), hook.getUUID(), hook.getName()); } @HookHandler public void onDisconnect(DisconnectionHook hook) throws UnknownHostException { core.updateClient(InetAddress.getByName(hook.getPlayer().getIP()), hook.getPlayer().getUUID(), hook.getPlayer().getName()); } } public final class PingListener implements PluginListener { private PingListener() {} @HookHandler public void onServerListPing(final ServerListPingHook hook) throws Exception { StatusRequest request = core.createRequest(hook.getRequesterAddress()); request.setProtocolVersion(hook.getRequesterProtocol()); request.setTarget(hook.getHostNamePinged(), hook.getPortPinged()); StatusResponse response = request.createResponse(core.getStatus(), new ResponseFetcher() { @Override public Integer getOnlinePlayers() { return hook.getCurrentPlayers(); } @Override public Integer getMaxPlayers() { return hook.getMaxPlayers(); } @Override public int getProtocolVersion() { return hook.getRequesterProtocol(); // :| } }); // Description String message = response.getDescription(); if (message != null) hook.setMotd(message); // Favicon FaviconSource favicon = response.getFavicon(); if (favicon != null) { Optional<String> icon = faviconCache.getUnchecked(favicon); if (icon.isPresent()) hook.setFavicon(icon.get()); } // Online players Integer count = response.getOnlinePlayers(); if (count != null) hook.setCurrentPlayers(count); // Max players count = response.getMaxPlayers(); if (count != null) hook.setMaxPlayers(count); // Player hover message = response.getPlayerHover(); if (message != null) { List<GameProfile> profiles = hook.getProfiles(); if (!(profiles instanceof ArrayList)) { profiles = new ArrayList<>(); PROFILES_FIELD.set(hook, profiles); } else { 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(new GameProfile(StatusManager.EMPTY_UUID, line)); } } else profiles.add(new GameProfile(StatusManager.EMPTY_UUID, message)); } } } @Override public ServerListPlusCore getCore() { return core; } @Override public ServerType getServerType() { return ServerType.CANARY; } @Override public String getServerImplementation() { return Canary.getImplementationTitle() + " v" + Canary.getServer().getCanaryModVersion() + " (MC: " + Canary.getServer().getName() + ')'; } @Override public Path getPluginFolder() { return pluginFolder; } @Override public Integer getOnlinePlayers(String worldName) { World world = Canary.getServer().getWorld(worldName); return world != null ? world.getPlayerList().size() : null; } @Override public Iterator<String> getRandomPlayers() { List<Player> players = Canary.getServer().getPlayerList(); 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 worldName) { World world = Canary.getServer().getWorld(worldName); if (world == null) return null; List<Player> players = world.getPlayerList(); List<String> result = new ArrayList<>(players.size()); for (Player player : players) { result.add(player.getName()); } return Randoms.shuffle(result).iterator(); } @Override public Cache<?, ?> getRequestCache() { return null; } @Override public LoadingCache<FaviconSource, ?> getFaviconCache() { return faviconCache; } @Override public void runAsync(Runnable task) { TaskManager.executeTask(task); } @Override public ScheduledTask scheduleAsync(Runnable task, long repeat, TimeUnit unit) { return new ScheduledCanaryTask(TaskManager.scheduleDelayedTask(task, repeat, unit)); } private static final Pattern COLOR_CODE = Pattern.compile("(?i)&([0-9A-FK-OR])"); @Override public String colorize(String s) { return COLOR_CODE.matcher(s).replaceAll(ChatFormat.MARKER + "$1"); } @Override public ServerListPlusLogger createLogger(ServerListPlusCore core) { return new Log4j2ServerListPlusLogger(core, getLogman()); } @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) { registerListener(this.loginListener = new LoginListener()); getLogman().debug("Registered proxy player tracking listener."); } } else if (loginListener != null) { Canary.hooks().unregisterPluginListener(loginListener); this.loginListener = null; getLogman().debug("Unregistered proxy player tracking listener."); } // Plugin statistics if (confs.get(PluginConf.class).Stats) { if (metrics == null) try { this.metrics = new MetricsLite(this); metrics.start(); } catch (Throwable e) { getLogman().debug("Failed to enable plugin statistics: {}", Helper.causedException(e)); } } else if (metrics != null) try { metrics.disable(); this.metrics = null; } catch (Throwable e) { getLogman().debug("Failed to disable plugin statistics: ", Helper.causedException(e)); } } @Override public void statusChanged(StatusManager status, boolean hasChanges) { // Status listener if (hasChanges) { if (pingListener == null) { registerListener(this.pingListener = new PingListener()); getLogman().debug("Registered proxy ping listener."); } } else if (pingListener != null) { Canary.hooks().unregisterPluginListener(pingListener); this.pingListener = null; getLogman().debug("Unregistered proxy ping listener."); } } @Override public boolean isBanned(PlayerIdentity playerIdentity) { return Canary.bans().isBanned(playerIdentity.getUuid().toString()); } }