package net.minecrell.serverlistplus.server;
import static com.google.common.base.Preconditions.checkState;
import static net.minecrell.serverlistplus.core.logging.Logger.ERROR;
import static net.minecrell.serverlistplus.core.logging.Logger.INFO;
import com.google.common.base.Splitter;
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.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import net.minecrell.serverlistplus.core.ServerListPlusCore;
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.JavaServerListPlusLogger;
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.replacement.ReplacementManager;
import net.minecrell.serverlistplus.core.replacement.util.Literals;
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.server.config.ServerConf;
import net.minecrell.serverlistplus.server.network.Netty;
import net.minecrell.serverlistplus.server.network.NetworkManager;
import net.minecrell.serverlistplus.server.status.Favicon;
import net.minecrell.serverlistplus.server.status.StatusClient;
import net.minecrell.serverlistplus.server.status.StatusPingResponse;
import net.minecrell.serverlistplus.server.status.UserProfile;
import net.minecrell.serverlistplus.server.util.FormattingCodes;
import java.awt.image.BufferedImage;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
public final class ServerListPlusServer implements ServerListPlusPlugin {
private static ServerListPlusServer instance;
private final ServerListPlusCore core;
private final Logger logger;
private final Path workingDir;
private final NetworkManager network;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private boolean started;
private boolean playerTracking;
private ImmutableList<String> loginMessages;
// 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.empty(); // Favicon loading failed
else return Optional.of(Favicon.create(image));
}
};
private LoadingCache<FaviconSource, Optional<String>> faviconCache;
public ServerListPlusServer(Logger logger) throws UnknownHostException {
checkState(instance == null, "Server was already initialized");
instance = this;
this.logger = logger;
this.workingDir = Paths.get("");
logger.log(INFO, "Loading...");
this.core = new ServerListPlusCore(this, new ServerProfileManager());
ServerConf conf = this.core.getConf(ServerConf.class);
this.network = new NetworkManager(this, Netty.parseAddress(conf.Address));
logger.log(INFO, "Successfully loaded!");
}
public boolean start() {
this.started = true;
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
try {
this.network.start();
} catch (Exception e) {
this.logger.log(ERROR, "Failed to start network manager", e);
this.stop();
return false;
}
return true;
}
public void join() throws InterruptedException {
this.network.join();
}
public boolean stop() {
if (this.started) {
this.logger.info("Stopping...");
try {
this.network.stop();
} catch (Exception e) {
this.logger.log(ERROR, "Failed to stop network manager", e);
return false;
}
this.core.stop();
this.started = false;
return true;
}
return false;
}
public static StatusPingResponse postLegacy(InetSocketAddress address, InetSocketAddress virtualHost) {
StatusPingResponse response = instance.handle(new StatusClient(address, OptionalInt.empty(), virtualHost));
response.getVersion().setProtocol(Byte.MAX_VALUE);
if (response.getPlayers() == null) {
response.setPlayers(new StatusPingResponse.Players(0, -1, null));
}
return response;
}
public static StatusPingResponse post(StatusClient client) {
return instance.handle(client);
}
public static String postLogin(StatusClient client, String name) {
return instance.handleLogin(client, name);
}
public String handleLogin(StatusClient client, String name) {
if (this.playerTracking) {
core.updateClient(client.getAddress().getAddress(), null, name);
}
String message = Randoms.nextEntry(this.loginMessages);
return Literals.replace(message, "%player%", name);
}
public StatusPingResponse handle(StatusClient client) {
StatusPingResponse ping = new StatusPingResponse();
StatusRequest request = core.createRequest(client.getAddress().getAddress());
client.getProtocol().ifPresent(request::setProtocolVersion);
InetSocketAddress host = client.getVirtualHost();
if (host != null) {
request.setTarget(host);
}
final StatusPingResponse.Players players = ping.getPlayers();
final StatusPingResponse.Version version = ping.getVersion();
StatusResponse response = request.createResponse(core.getStatus(),
// Return unknown player counts if it has been hidden
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 version != null ? version.getProtocol() : 0;
}
});
// Description
String message = response.getDescription();
if (message != null) ping.setDescription(message);
if (version != null) {
// Version name
message = response.getVersion();
if (message != null) version.setName(message);
// Protocol version
Integer protocol = response.getProtocolVersion();
if (protocol != null) version.setProtocol(protocol);
}
// Favicon
FaviconSource favicon = response.getFavicon();
if (favicon != null) {
Optional<String> icon = faviconCache.getUnchecked(favicon);
if (icon.isPresent()) ping.setFavicon(icon.get());
}
if (response.hidePlayers()) {
ping.setPlayers(null);
} else {
StatusPingResponse.Players newPlayers = players;
if (newPlayers == null) {
newPlayers = new StatusPingResponse.Players(0, 0, null);
ping.setPlayers(newPlayers);
}
// Online players
Integer count = response.getOnlinePlayers();
if (count != null) newPlayers.setOnline(count);
// Max players
count = response.getMaxPlayers();
if (count != null) newPlayers.setMax(count);
// Player hover
message = response.getPlayerHover();
if (message != null) {
if (response.useMultipleSamples()) {
count = response.getDynamicSamples();
List<String> lines = count != null ? Helper.splitLinesCached(message, count) :
Helper.splitLinesCached(message);
UserProfile[] sample = new UserProfile[lines.size()];
for (int i = 0; i < sample.length; i++)
sample[i] = new UserProfile(lines.get(i), StatusManager.EMPTY_UUID);
newPlayers.setSample(sample);
} else
newPlayers.setSample(new UserProfile[]{
new UserProfile(message, StatusManager.EMPTY_UUID) });
}
}
return ping;
}
private static final ImmutableSet<String> COMMAND_ALIASES = ImmutableSet.of("serverlistplus", "serverlist+",
"serverlist", "slp", "sl+", "s++", "serverping+", "serverping", "spp", "slus");
private static final Splitter COMMAND_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings();
public boolean processCommand(String command) {
if (command.charAt(0) == '/') {
command = command.substring(1);
}
String root = command;
int pos = command.indexOf(' ');
if (pos >= 0) {
root = command.substring(0, pos).toLowerCase(Locale.ENGLISH);
}
if (COMMAND_ALIASES.contains(root)) {
if (pos >= 0) {
command = command.substring(pos + 1);
} else {
command = "";
}
} else {
root = "serverlistplus";
}
command = command.trim();
List<String> args = COMMAND_SPLITTER.splitToList(command);
String subcommand = args.isEmpty() ? "" : args.get(0);
if (subcommand.equalsIgnoreCase("stop")) {
return this.stop();
}
this.core.executeCommand(ConsoleCommandSender.INSTANCE, root, args.toArray(new String[args.size()]));
if (subcommand.equalsIgnoreCase("help")) {
ConsoleCommandSender.INSTANCE.sendMessage("/slp stop - Stop the server.");
}
return false;
}
@Override
public ServerListPlusCore getCore() {
return this.core;
}
@Override
public ServerType getServerType() {
return ServerType.SERVER;
}
@Override
public String getServerImplementation() {
return "ServerListPlusServer";
}
@Override
public Path getPluginFolder() {
return this.workingDir;
}
@Override
public Integer getOnlinePlayers(String location) {
return null;
}
@Override
public Iterator<String> getRandomPlayers() {
return null;
}
@Override
public Iterator<String> getRandomPlayers(String location) {
return null;
}
@Override
public Cache<?, ?> getRequestCache() {
return null;
}
@Override
public LoadingCache<FaviconSource, Optional<String>> getFaviconCache() {
return this.faviconCache;
}
@Override
public void runAsync(Runnable task) {
this.scheduler.execute(task);
}
@Override
public ScheduledTask scheduleAsync(Runnable task, long repeat, TimeUnit unit) {
return new ScheduledFutureTask(this.scheduler.scheduleAtFixedRate(task, 0, repeat, unit));
}
@Override
public String colorize(String s) {
return FormattingCodes.colorize(s);
}
@Override
public ServerListPlusLogger createLogger(ServerListPlusCore core) {
return new JavaServerListPlusLogger(this.core, this.logger);
}
@Override
public void initialize(ServerListPlusCore core) {
core.registerConf(ServerConf.class, new ServerConf(), ServerConf.getExample(), "Server");
}
@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) {
this.playerTracking = confs.get(PluginConf.class).PlayerTracking.Enabled;
ServerConf conf = confs.get(ServerConf.class);
ImmutableList.Builder<String> builder = ImmutableList.builder();
for (String message : conf.Login.Message) {
builder.add(ReplacementManager.replaceStatic(core, message));
}
this.loginMessages = builder.build();
}
@Override
public void statusChanged(StatusManager status, boolean hasChanges) {
}
@Override
public boolean isBanned(PlayerIdentity playerIdentity) {
return false;
}
}