Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Dreeam committed 2024-01-31 20:25:14 -05:00
commit 4efa64d1d8
11 files changed
+862 -682

No files matched your search

+4 -4
View File
@@ -1,5 +1,5 @@
.idea
*.iml
.idea/
.gradle/
build/
.gradle
build
*.iml
+13 -27
View File
@@ -1,37 +1,23 @@
plugins {
`java-library`
java
id("com.github.johnrengelman.shadow") version ("7.0.0")
}
group = "com.extendedclip.papi.expansion.server"
version = "2.6.2"
description = "PlaceholderAPI expansion for server placeholders"
group = "at.helpch.placeholderapi.expansion"
version = "3.0.0"
repositories {
mavenCentral()
maven {
name = "paper-repo"
url = uri("https://repo.papermc.io/repository/maven-public/")
}
maven {
name = "placeholderapi"
url = uri("https://repo.extendedclip.com/content/repositories/placeholderapi/")
}
maven("https://papermc.io/repo/repository/maven-public/")
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
}
dependencies {
compileOnly("com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.5") {
exclude(group = "org.bstats")
compileOnly("io.papermc.paper:paper-api:1.17.1-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.5")
}
tasks {
shadowJar {
archiveFileName.set("PAPI-Expansion-Server-${project.version}.jar")
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'Server-Expansion'
@@ -0,0 +1,255 @@
package at.helpch.placeholderapi.expansion.server;
import at.helpch.placeholderapi.expansion.server.util.Logging;
import at.helpch.placeholderapi.expansion.server.util.ServerUtil;
import at.helpch.placeholderapi.expansion.server.util.TimeFormatter;
import at.helpch.placeholderapi.expansion.server.util.TpsFormatter;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.Cacheable;
import me.clip.placeholderapi.expansion.Configurable;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.management.ManagementFactory;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.ToIntFunction;
public final class ServerExpansion extends PlaceholderExpansion implements Cacheable, Configurable {
private final Cache<String, Integer> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
private final Runtime runtime = Runtime.getRuntime();
/**
* Constant that represents how many bytes are in a mebibyte
* <br/>
* Required for the {@code ram_} placeholders, as the values provided by the system are in bytes
*/
private final int MiB = 1_048_576;
// ----- Values from config
private String serverName;
// -----
private TpsFormatter tpsFormatter;
private TimeFormatter timeFormatter;
@Override
public @NotNull String getIdentifier() {
return "server";
}
@Override
public @NotNull String getAuthor() {
return "HelpChat";
}
@Override
public @NotNull String getVersion() {
return "3.0.0";
}
@Override
public boolean canRegister() {
this.serverName = getString("server_name", "A Minecraft Server");
this.tpsFormatter = new TpsFormatter(
getString("tps_color.low", "&c"),
getString("tps_color.medium", "&e"),
getString("tps_color.high", "&a")
);
this.timeFormatter = new TimeFormatter(
Locale.forLanguageTag(this.getString("time.locale", Locale.getDefault().toLanguageTag())),
ZoneId.of(this.getString("time.zone", ZoneId.systemDefault().getId())),
Optional.ofNullable(getConfigSection("time.suffix")).map(section -> section.getValues(false)).orElseGet(HashMap::new)
);
return true;
}
@Override
public void clear() {
cache.invalidateAll();
timeFormatter = null;
tpsFormatter = null;
}
@Override
public Map<String, Object> getDefaults() {
return ImmutableMap.<String, Object>builder()
.put("server_name", "A Minecraft Server")
.put("time.locale", Locale.getDefault().toLanguageTag())
.put("time.zone", ZoneId.systemDefault().getId())
.put("time.suffix.week", "w")
.put("time.suffix.day", "d")
.put("time.suffix.hour", "h")
.put("time.suffix.minute", "m")
.put("time.suffix.second", "s")
.put("tps_color.high", "&a")
.put("tps_color.medium", "&e")
.put("tps_color.low", "&c")
.build();
}
/**
* Get some data from all worlds
*
* @param function data to get (e.g. total entities)
* @return sum
*/
private int getFromAllWorlds(@NotNull final ToIntFunction<? super World> function) {
return Bukkit.getWorlds()
.stream()
.mapToInt(function)
.sum();
}
/**
* Get a value from the {@link #cache}
*
* @param key key
* @param callable cache method
* @return value if found otherwise empty
*/
private @NotNull String getFromCache(@NotNull final String key, @NotNull final Callable<Integer> callable) {
try {
return String.valueOf(cache.get(key, callable));
} catch (ExecutionException e) {
Logging.error(e, "Could not get key \"{0}\" from cache", key);
return "";
}
}
/**
* Format a {@code boolean} as {@link PlaceholderAPIPlugin#booleanTrue()} or {@link PlaceholderAPIPlugin#booleanFalse()}
*
* @param bool boolean
* @return {@link PlaceholderAPIPlugin#booleanTrue()} if the boolean is true, otherwise {@link PlaceholderAPIPlugin#booleanFalse()}
*/
private @NotNull String bool(final boolean bool) {
return bool ? PlaceholderAPIPlugin.booleanTrue() : PlaceholderAPIPlugin.booleanFalse();
}
/**
* Get the number of online players from a world
*
* @param worldName the name of the world
* @return {@code -1} if {@code worldName} is empty or the world doesn't exist, otherwise {@link World#getPlayers()}.size()
*/
private int getOnlinePlayers(@NotNull final String worldName) {
if (worldName.trim().isEmpty()) {
return -1;
}
return Optional.ofNullable(Bukkit.getWorld(worldName))
.map(World::getPlayers)
.map(List::size)
.orElse(-1);
}
@SuppressWarnings("SpellCheckingInspection")
@Override
public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) {
switch (params) {
// ----- Players placeholders
case "online":
return String.valueOf(Bukkit.getOnlinePlayers().size());
case "max_players":
return String.valueOf(Bukkit.getMaxPlayers());
case "unique_joins":
return String.valueOf(Bukkit.getOfflinePlayers().length);
// -----
// ----- Server software placeholders
case "name":
return this.serverName;
case "variant":
return ServerUtil.getVariant();
case "version":
return ServerUtil.getServerVersion();
case "build":
return ServerUtil.getBuild();
case "version_build":
case "version_full":
return ServerUtil.getServerVersion() + '-' + ServerUtil.getBuild();
// -----
// ----- RAM placeholders
case "ram_used":
return String.valueOf((runtime.totalMemory() - runtime.freeMemory()) / MiB);
case "ram_free":
return String.valueOf(runtime.freeMemory() / MiB);
case "ram_total":
return String.valueOf(runtime.totalMemory() / MiB);
case "ram_max":
return String.valueOf(runtime.maxMemory() / MiB);
// -----
case "tps":
return tpsFormatter.getTps(null);
case "uptime":
return timeFormatter.formatTimeInSeconds(TimeUnit.MILLISECONDS.toSeconds(ManagementFactory.getRuntimeMXBean().getUptime()));
case "total_chunks":
return getFromCache("chunks", () -> getFromAllWorlds(world -> world.getLoadedChunks().length));
case "total_living_entities":
return getFromCache("livingEntities", () -> getFromAllWorlds(world -> world.getLivingEntities().size()));
case "total_entities":
return getFromCache("totalEntities", () -> getFromAllWorlds(world -> world.getEntities().size()));
case "has_whitelist":
return bool(Bukkit.hasWhitelist());
}
// tps_<type>
if (params.startsWith("tps_")) {
return tpsFormatter.getTps(params.substring("tps_".length()));
}
// online_<world name>
if (params.startsWith("online_")) {
return String.valueOf(getOnlinePlayers(params.substring("online_".length())));
}
// time_<simple date format>
if (params.startsWith("time_")) {
return timeFormatter.formatTime(params.substring("time_".length()));
}
// countdown_raw_<date> or countdown_raw_<custom format>_<date>
if (params.startsWith("countdown_raw_")) {
return timeFormatter.calculateTimeBetweenWithoutFormat(player, params.substring("countdown_raw_".length()), true);
}
// countdown_<date> or countdown_<custom format>_<date>
if (params.startsWith("countdown_")) {
return timeFormatter.calculateTimeBetween(player, params.substring("countdown_".length()), true, true);
}
// countup_raw_<date> or countup_raw_<custom format>_<date>
if (params.startsWith("countup_raw_")) {
return timeFormatter.calculateTimeBetweenWithoutFormat(player, params.substring("countup_raw_".length()), false);
}
// countup_<date> or countup_<custom format>_<date>
if (params.startsWith("countup_")) {
return timeFormatter.calculateTimeBetween(player, params.substring("countup_".length()), false, true);
}
return null;
}
}
@@ -0,0 +1,26 @@
package at.helpch.placeholderapi.expansion.server.util;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.MessageFormat;
import java.util.logging.Level;
public final class Logging {
private static void log(@NotNull final Level level, @Nullable final Throwable throwable, @NotNull final String message, @NotNull final Object... args) {
PlaceholderAPIPlugin.getInstance()
.getLogger()
.log(level, "[server] " + MessageFormat.format(message, args), throwable);
}
public static void warn(@NotNull final String message, @NotNull final Object... args) {
log(Level.WARNING, null, message, args);
}
public static void error(@NotNull final Throwable throwable, @NotNull final String message, @NotNull final Object... args) {
log(Level.SEVERE, throwable, message, args);
}
}
@@ -0,0 +1,158 @@
package at.helpch.placeholderapi.expansion.server.util;
import org.bukkit.Bukkit;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.TreeMap;
public final class ServerUtil {
private static final Map<String, String> variants = new TreeMap<>();
private static final String variant;
private static final String serverVersion;
private static final String build;
// TPS stuff
private static final Object craftServer;
private static final Field tpsField;
private static boolean hasTpsMethod; // Paper and its forks have Bukkit#getTps
static {
variants.put("net.pl3x.purpur.PurpurConfig", "Purpur");
variants.put("gg.airplane.AirplaneConfig", "Airplane");
variants.put("com.tuinity.tuinity.config.TuinityConfig", "Tuinity");
variants.put("io.papermc.paper.configuration.ConfigurationLoaders", "Paper"); // New config location for Paper 1.19+
variants.put("com.destroystokyo.paper.PaperConfig", "Paper"); // Still supported by Paper, but deprecated.
craftServer = getCraftServer();
tpsField = getTpsHandler();
variant = findVariant();
serverVersion = Bukkit.getBukkitVersion().split("-")[0]; // The version is formatted as 1.20.1-R0.1-SNAPSHOT
build = findBuild();
}
private static String findVariant() {
for (final Map.Entry<String, String> entry : variants.entrySet()) {
try {
Class.forName(entry.getKey());
return entry.getValue();
} catch (ClassNotFoundException ignored) { }
}
// Source: https://github.com/PaperMC/Paper/blob/f7717c3/patches/server/0027-Show-Paper-in-client-crashes-server-lists-and-Mojang.patch#L17
if (craftServer != null) {
try {
return (String) craftServer.getClass()
.getMethod("getServerModName")
.invoke(craftServer);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ignored) { }
}
try {
Class.forName("org.spigotmc.SpigotConfig");
return "Spigot";
} catch (ClassNotFoundException ignored) {
return "Unknown"; // Not even spigot??
}
}
private static Object getCraftServer() {
try {
Class<?> clas;
if (VersionHelper.IS_1_17_OR_HIGHER) {
clas = Class.forName("net.minecraft.server.MinecraftServer");
} else {
clas = Class.forName(String.format("net.minecraft.server.%s.MinecraftServer", VersionHelper.NMS_VERSION));
}
return clas.getMethod("getServer").invoke(null);
} catch (ClassNotFoundException e) {
Logging.error(e, "Could not find class MinecraftServer");
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
Logging.error(e, "Could not invoke method MinecraftServer#getServer");
}
return null;
}
private static Field getTpsHandler() {
try {
Bukkit.class.getMethod("getTPS");
hasTpsMethod = true;
return null;
} catch (NoSuchMethodException ignored) { }
if (craftServer == null) {
Logging.warn("Could not get field 'recentTps' from class MinecraftServer because variable 'craftServer' is null");
return null;
}
try {
return craftServer.getClass().getField("recentTps");
} catch (NoSuchFieldException e) {
Logging.error(e, "Could not find field 'recentTps' in class {0}", craftServer.getClass().getName());
return null;
}
}
private static String findBuild() {
String[] buildParts = Bukkit.getVersion().split("-");
switch (getVariant().toLowerCase()) {
case "spigot":
return buildParts[0];
// Source: https://github.com/PaperMC/Paper/blob/0ae58c0/patches/server/0001-Setup-Gradle-project.patch#L75
case "paper":
// Source: https://github.com/Tuinity/Tuinity/blob/4c31ea1/patches/server/0001-Build-changes.patch#L34
case "tuinity":
// Source: https://github.com/PurpurMC/Purpur/blob/c049bfb/patches/server/0002-Rebrand.patch#L44
case "purpur":
// Source: https://github.com/TECHNOVE/Airplane/blob/af3563c/patches/server/0002-Airplane-Branding-Changes.patch#L41
case "airplane": {
// Format: git-<Paper/Tuinity/Purpur/Airplane>-<version>
if (buildParts.length >= 3) {
if (buildParts[2].contains(" ")) {
return buildParts[2].substring(0, buildParts[2].indexOf(" "));
} else {
return buildParts[2];
}
}
}
}
return "Unknonw";
}
public static String getVariant() {
return variant;
}
public static String getServerVersion() {
return serverVersion;
}
public static String getBuild() {
return build;
}
public static double[] getTps() {
if (hasTpsMethod) {
return Bukkit.getTPS();
}
if (craftServer == null || tpsField == null) {
return new double[]{0, 0, 0};
}
try {
return (double[]) tpsField.get(craftServer);
} catch (IllegalAccessException e) {
return new double[]{0, 0, 0};
}
}
}
@@ -0,0 +1,169 @@
package at.helpch.placeholderapi.expansion.server.util;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
public final class TimeFormatter {
private final Map<String, DateTimeFormatter> dateFormatsCache = new HashMap<>();
private final Locale timeLocale;
private final ZoneId timeZone;
private final Map<@NotNull String, @Nullable Object> timeUnitSuffixes;
public TimeFormatter(
@NotNull final Locale timeLocale, @NotNull final ZoneId timeZone,
@NotNull Map<@NotNull String, @Nullable Object> timeUnitSuffixes
) {
this.timeLocale = timeLocale;
this.timeZone = timeZone;
this.timeUnitSuffixes = timeUnitSuffixes;
}
/**
* Parse a string to a {@link DateTimeFormatter} and cache it
*
* @param pattern pattern to parse
* @return {@link DateTimeFormatter} if the argument is a valid pattern, otherwise null
*/
public @Nullable DateTimeFormatter parseFormat(@NotNull final String pattern) {
return dateFormatsCache.computeIfAbsent(pattern, __ -> {
try {
return DateTimeFormatter.ofPattern(pattern, timeLocale);
} catch (IllegalArgumentException e) {
Logging.warn("Could not parse DateTimeFormatter from string \"{0}\"", pattern);
return null;
}
});
}
/**
* Format the current time with the given format
*
* @param format format
* @return {@code null} if the format is empty or invalid, otherwise {@link LocalDateTime#now(ZoneId)} formatted
*/
public @Nullable String formatTime(@NotNull final String format) {
if (format.trim().isEmpty()) {
return null;
}
return Optional.ofNullable(parseFormat(format))
.map(formatter -> LocalDateTime.now(timeZone).format(formatter))
.orElse(null);
}
/**
* Format time as {@code #w #d #h #m #s}
*
* @param time time in seconds
* @return time formatted
*/
public @NotNull String formatTimeInSeconds(long time) {
final StringJoiner joiner = new StringJoiner(" ");
final BiConsumer<@NotNull Long, @Nullable Object> appendTime = (value, unit) -> {
if (value > 0 && unit != null) {
joiner.add(value + unit.toString());
}
};
long seconds = time;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
final long weeks = days / 7;
seconds %= 60;
minutes %= 60;
hours %= 24;
days %= 7;
appendTime.accept(weeks, timeUnitSuffixes.get("week"));
appendTime.accept(days, timeUnitSuffixes.get("day"));
appendTime.accept(hours, timeUnitSuffixes.get("hour"));
appendTime.accept(minutes, timeUnitSuffixes.get("minute"));
appendTime.accept(seconds, timeUnitSuffixes.get("second"));
return joiner.toString();
}
/**
* Calculate the time between {@link LocalDateTime#now(ZoneId)} and another date and return a formatted value
* using {@link #formatTimeInSeconds(long)} if {@code formatTime} is {@code true}.
*
* @param player player
* @param args arguments received on {@link me.clip.placeholderapi.expansion.PlaceholderExpansion#onRequest(OfflinePlayer, String)}
* @param isCountdown whether the time calculated is for a countdown or a count-up
* @param formatTime whether the time should be formatted with {@link #formatTimeInSeconds(long)}
* @return time formatter
*/
public @NotNull String calculateTimeBetween(
@Nullable final OfflinePlayer player, @NotNull final String args,
final boolean isCountdown, final boolean formatTime
) {
DateTimeFormatter formatter;
String otherDateString;
// Placeholder with custom time format
if (args.contains("_")) {
final String[] parts = PlaceholderAPI.setBracketPlaceholders(player, args).split("_");
if (parts.length != 2) {
return "invalid format and time";
}
formatter = this.parseFormat(parts[0]);
otherDateString = parts[1];
} else {
formatter = this.parseFormat(PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().dateFormat());
otherDateString = args;
}
if (formatter == null) {
return "invalid date format";
}
try {
LocalDateTime now = LocalDateTime.now(timeZone);
LocalDateTime otherDate = LocalDateTime.parse(otherDateString, formatter);
if (otherDate.isEqual(now)) {
return "0";
}
// 'otherDate' should be after 'now' for countdowns and before for count-ups
if ((isCountdown && otherDate.isBefore(now)) || (!isCountdown && otherDate.isAfter(now))) {
return "0";
}
final long time = isCountdown ? now.until(otherDate, ChronoUnit.SECONDS) : otherDate.until(now, ChronoUnit.SECONDS);
return formatTime ? this.formatTimeInSeconds(time) : String.valueOf(time);
} catch (DateTimeParseException e) {
final String type = isCountdown ? "countdown" : "count-up";
Logging.error(e, "Could not calculate {0} (format: \"{1}\", other date: \"{2}\")", type, formatter.toString(), otherDateString);
return "invalid date";
}
}
public @NotNull String calculateTimeBetweenWithoutFormat(
@Nullable final OfflinePlayer player, @NotNull final String args,
final boolean isCountdown
) {
return calculateTimeBetween(player, args, isCountdown, false);
}
}
@@ -0,0 +1,132 @@
package at.helpch.placeholderapi.expansion.server.util;
import org.bukkit.ChatColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.StringJoiner;
import java.util.function.Function;
public class TpsFormatter {
private final String lowColor;
private final String mediumColor;
private final String highColor;
/**
* @param lowColor color for {@code tps < 16.0}
* @param mediumColor color for {@code tps < 18.0}
* @param highColor color for {@code tps > 18.0}
*/
public TpsFormatter(@NotNull final String lowColor, @NotNull final String mediumColor, @NotNull final String highColor) {
this.lowColor = ChatColor.translateAlternateColorCodes('&', lowColor);
this.mediumColor = ChatColor.translateAlternateColorCodes('&', mediumColor);
this.highColor = ChatColor.translateAlternateColorCodes('&', highColor);
}
/**
* Round the tps and append a {@code *} before if the value is higher than 20.0
*
* @param tps tps
* @return tps rounded
*/
private @NotNull String round(final double tps) {
final double finalTps = Math.min(Math.round(tps), 20.0);
return (tps > 20.0 ? "*" : "") + finalTps;
}
/**
* Get the color based on the tps value
*
* @param tps tps
* @return color for the tps
*/
private @NotNull String getColor(final double tps) {
return (tps > 18.0) ? highColor : (tps > 16.0) ? mediumColor : lowColor;
}
private @NotNull String getPercent(final double tps) {
final double percent = Math.min(Math.round(100 / 20.0 * tps), 100.0);
return (tps > 20.0 ? "*" : "") + percent + '%';
}
private @NotNull String getColoredTps(final double tps) {
return getColor(tps) + round(tps);
}
private @NotNull String getColoredTpsPercent(final double tps) {
return getColor(tps) + getPercent(tps);
}
private @NotNull String getAllTps(@NotNull final Function<Double, String> formatter) {
final StringJoiner joiner = new StringJoiner(ChatColor.GRAY + ", ");
for (final double tps : ServerUtil.getTps()) {
joiner.add(formatter.apply(tps));
}
return joiner.toString();
}
public @Nullable String getTps(@Nullable final String type) {
if (type == null || type.isEmpty()) {
return getAllTps(this::getColoredTps);
}
switch (type) {
case "1":
case "one":
return round(ServerUtil.getTps()[0]);
case "5":
case "five":
return round(ServerUtil.getTps()[1]);
case "15":
case "fifteen":
return round(ServerUtil.getTps()[2]);
case "1_colored":
case "one_colored":
return getColoredTps(ServerUtil.getTps()[0]);
case "5_colored":
case "five_colored":
return getColoredTps(ServerUtil.getTps()[1]);
case "15_colored":
case "fifteen_colored":
return getColoredTps(ServerUtil.getTps()[2]);
case "percent":
return getAllTps(this::getColoredTpsPercent);
case "1_percent":
case "one_percent":
return getPercent(ServerUtil.getTps()[0]);
case "5_percent":
case "five_percent":
return getPercent(ServerUtil.getTps()[1]);
case "15_percent":
case "fifteen_percent":
return getPercent(ServerUtil.getTps()[2]);
case "1_percent_colored":
case "one_percent_colored":
return getColoredTpsPercent(ServerUtil.getTps()[0]);
case "5_percent_colored":
case "five_percent_colored":
return getColoredTpsPercent(ServerUtil.getTps()[1]);
case "15_percent_colored":
case "fifteen_percent_colored":
return getColoredTpsPercent(ServerUtil.getTps()[2]);
}
return null;
}
}
@@ -0,0 +1,104 @@
/**
* MIT License
* <p>
* Copyright (c) 2021 TriumphTeam
* <p>
* 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:
* <p>
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* <p>
* 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 at.helpch.placeholderapi.expansion.server.util;
import com.google.common.primitives.Ints;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class for detecting server version for legacy support :(
*/
public final class VersionHelper {
public static final String NMS_VERSION = getNmsVersion();
private static final int V1_17 = 1_17_0;
private static final int CURRENT_VERSION = getCurrentVersion();
private static final boolean IS_PAPER = checkPaper();
public static final boolean IS_1_17_OR_HIGHER = CURRENT_VERSION >= V1_17;
/**
* Check if the server has access to the Paper API
* Taken from <a href="https://github.com/PaperMC/PaperLib">PaperLib</a>
*
* @return True if on Paper server (or forks), false anything else
*/
private static boolean checkPaper() {
final String[] configClasses = {"io.papermc.paper.configuration.ConfigurationLoaders", "com.destroystokyo.paper.PaperConfig"};
for (String configClass : configClasses) {
try {
Class.forName(configClass);
return true;
} catch (ClassNotFoundException ignored) { }
}
return false;
}
/**
* Gets the current server version
*
* @return A protocol like number representing the version, for example 1.16.5 - 1165
*/
private static int getCurrentVersion() {
// No need to cache since will only run once
final Matcher matcher = Pattern.compile("(?<version>\\d+\\.\\d+)(?<patch>\\.\\d+)?").matcher(Bukkit.getBukkitVersion());
final StringBuilder stringBuilder = new StringBuilder();
if (matcher.find()) {
stringBuilder.append(matcher.group("version").replace(".", ""));
final String patch = matcher.group("patch");
if (patch == null) stringBuilder.append("0");
else stringBuilder.append(patch.replace(".", ""));
}
//noinspection UnstableApiUsage
final Integer version = Ints.tryParse(stringBuilder.toString());
// Should never fail
if (version == null) throw new IllegalArgumentException("Could not retrieve server version!");
return version;
}
private static String getNmsVersion() {
final String version = Bukkit.getServer().getClass().getPackage().getName();
return version.substring(version.lastIndexOf('.') + 1);
}
public static Class<?> craftClass(@NotNull final String name) throws ClassNotFoundException {
return Class.forName("org.bukkit.craftbukkit." + NMS_VERSION + "." + name);
}
}
@@ -1,506 +0,0 @@
/*
*
* Server-Expansion
* Copyright (C) 2018 Ryan McCarthy
*
* 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 com.extendedclip.papi.expansion.server;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.Cacheable;
import me.clip.placeholderapi.expansion.Configurable;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.management.ManagementFactory;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public class ServerExpansion extends PlaceholderExpansion implements Cacheable, Configurable {
private ServerUtils serverUtils = null;
private final Map<String, SimpleDateFormat> dateFormats = new HashMap<>();
private final Runtime runtime = Runtime.getRuntime();
// config stuff
private String serverName;
private String low = "&c";
private String medium = "&e";
private String high = "&a";
// -----
private final Cache<String, Integer> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@Override
public boolean canRegister() {
serverName = this.getString("server_name", "A Minecraft Server");
low = this.getString("tps_color.low", "&c");
medium = this.getString("tps_color.medium", "&e");
high = this.getString("tps_color.high", "&a");
return true;
}
@Override
public void clear() {
dateFormats.clear();
serverUtils = null;
cache.invalidateAll();
}
@Override
public @NotNull String getIdentifier() {
return "server";
}
@Override
public @NotNull String getAuthor() {
return "clip";
}
@Override
public @NotNull String getVersion() {
return "2.6.2";
}
@Override
public Map<String, Object> getDefaults() {
final Map<String, Object> defaults = new HashMap<>();
defaults.put("tps_color.high", "&a");
defaults.put("tps_color.medium", "&e");
defaults.put("tps_color.low", "&c");
defaults.put("server_name", "A Minecraft Server");
return defaults;
}
private @Nullable String getCached(String key, Callable<Integer> callable) {
try {
return String.valueOf(cache.get(key, callable));
} catch (ExecutionException e) {
if (getPlaceholderAPI().getPlaceholderAPIConfig().isDebugMode()) {
getPlaceholderAPI().getLogger().log(Level.SEVERE, "[server] Could not access cache key " + key, e);
}
return "";
}
}
private int getChunks(){
return Bukkit.getWorlds()
.stream()
.mapToInt(world -> world.getLoadedChunks().length)
.sum();
}
private int getLivingEntities(){
return Bukkit.getWorlds()
.stream()
.mapToInt(world -> world.getLivingEntities().size())
.sum();
}
private Integer getTotalEntities(){
return Bukkit.getWorlds()
.stream()
.mapToInt(world -> world.getEntities().size())
.sum();
}
// Per world counts
private int getLocalChunks(String world){
return Bukkit.getWorld(world).getLoadedChunks().length;
}
private int getLocalPlayers(String world){
return Bukkit.getWorld(world).getPlayers().size();
}
private int getLocalEntities(String world){
return Bukkit.getWorld(world).getEntities().size();
}
private int getLocalLivingEntities(String world){
return Bukkit.getWorld(world).getLivingEntities().size();
}
// Paper method
private int getLocalTileEntities(String world){
return Bukkit.getWorld(world).getTileEntityCount();
}
private int getLocalTickableTileEntities(String world){
return Bukkit.getWorld(world).getTickableTileEntityCount();
}
@Override
public String onRequest(OfflinePlayer p, @NotNull String identifier) {
final int MB = 1048576;
if (serverUtils == null) {
serverUtils = new ServerUtils();
}
switch (identifier) {
// Players placeholders
case "online":
return String.valueOf(Bukkit.getOnlinePlayers().size());
case "max_players":
return String.valueOf(Bukkit.getMaxPlayers());
case "unique_joins":
return String.valueOf(Bukkit.getOfflinePlayers().length);
// -----
// Version placeholders
case "version":
return serverUtils.getVersion();
case "build":
return serverUtils.getBuild();
case "version_build":
case "version_full":
return serverUtils.getVersion() + '-' + serverUtils.getBuild();
// -----
// Ram placeholders
case "ram_used":
return String.valueOf((runtime.totalMemory() - runtime.freeMemory()) / MB);
case "ram_free":
return String.valueOf(runtime.freeMemory() / MB);
case "ram_total":
return String.valueOf(runtime.totalMemory() / MB);
case "ram_max":
return String.valueOf(runtime.maxMemory() / MB);
// -----
// Identity placeholders
case "name":
return serverName == null ? "" : serverName;
case "variant":
return serverUtils.getServerVariant();
// -----
// Other placeholders
case "tps":
return getTps(null);
case "uptime":
long seconds = TimeUnit.MILLISECONDS.toSeconds(ManagementFactory.getRuntimeMXBean().getUptime());
return formatTime(Duration.of(seconds, ChronoUnit.SECONDS));
case "total_chunks":
return getCached("chunks", this::getChunks);
case "total_living_entities":
return getCached("livingEntities", this::getLivingEntities);
case "total_entities":
return getCached("totalEntities", this::getTotalEntities);
case "has_whitelist":
return Bukkit.getServer().hasWhitelist() ? PlaceholderAPIPlugin.booleanTrue() : PlaceholderAPIPlugin.booleanFalse();
}
if (identifier.startsWith("tps_")) {
identifier = identifier.replace("tps_", "");
return getTps(identifier);
}
if (identifier.startsWith("online_")) {
identifier = identifier.replace("online_", "");
int i = 0;
for (Player o : Bukkit.getOnlinePlayers()) {
if (o.getWorld().getName().equals(identifier)) {
i = i + 1;
}
}
return String.valueOf(i);
}
if (identifier.startsWith("countdown_")) {
String time = identifier.replace("countdown_", "");
if (!time.contains("_")) {
Date then;
try {
then = PlaceholderAPIPlugin.getDateFormat().parse(time);
} catch (Exception e) {
return null;
}
Date now = new Date();
long between = then.getTime() - now.getTime();
if (between <= 0) {
return "0";
}
return formatTime(Duration.of((int) TimeUnit.MILLISECONDS.toSeconds(between), ChronoUnit.SECONDS));
} else {
String[] parts = PlaceholderAPI.setBracketPlaceholders(p, time).split("_");
if (parts.length != 2) {
return "invalid format and time";
}
time = parts[1];
String format = parts[0];
SimpleDateFormat f;
try {
f = new SimpleDateFormat(format);
} catch (Exception e) {
return "invalid date format";
}
Date then;
try {
then = f.parse(time);
} catch (Exception e) {
return "invalid date";
}
long t = System.currentTimeMillis();
long between = then.getTime() - t;
if (between <= 0) {
return "0";
}
return formatTime(Duration.of((int) TimeUnit.MILLISECONDS.toSeconds(between), ChronoUnit.SECONDS));
}
}
if (identifier.startsWith("time_")) {
identifier = identifier.replace("time_", "");
if (dateFormats.containsKey(identifier)) {
return dateFormats.get(identifier).format(new Date());
}
try {
SimpleDateFormat format = new SimpleDateFormat(identifier);
dateFormats.put(identifier, format);
return format.format(new Date());
} catch (NullPointerException | IllegalArgumentException ex) {
return null;
}
}
// Per world counts placeholders
if (identifier.startsWith("total_chunks_")) {
identifier = identifier.replace("total_chunks_", "");
return String.valueOf(getLocalChunks(identifier));
}
if (identifier.startsWith("total_players_")) {
identifier = identifier.replace("total_players_", "");
return String.valueOf(getLocalPlayers(identifier));
}
if (identifier.startsWith("total_entities_")) {
identifier = identifier.replace("total_entities_", "");
return String.valueOf(getLocalEntities(identifier));
}
if (identifier.startsWith("total_living_entities_")) {
identifier = identifier.replace("total_living_entities_", "");
return String.valueOf(getLocalLivingEntities(identifier));
}
if (identifier.startsWith("total_tile_entities_")) {
identifier = identifier.replace("total_tile_entities_", "");
return String.valueOf(getLocalTileEntities(identifier));
}
if (identifier.startsWith("total_tick_tile_entities_")) {
identifier = identifier.replace("total_tick_tile_entities_", "");
return String.valueOf(getLocalTickableTileEntities(identifier));
}
return null;
}
public String getTps(String arg) {
if (serverUtils.getServerVariant().equals("Folia")) return null;
if (arg == null || arg.isEmpty()) {
StringJoiner joiner = new StringJoiner(ChatColor.GRAY + ", ");
for (double tps : serverUtils.getTps()) {
joiner.add(getColoredTps(tps));
}
return joiner.toString();
}
switch (arg) {
case "1":
case "one":
return fix(serverUtils.getTps()[0]);
case "5":
case "five":
return fix(serverUtils.getTps()[1]);
case "15":
case "fifteen":
return fix(serverUtils.getTps()[2]);
case "1_colored":
case "one_colored":
return getColoredTps(serverUtils.getTps()[0]);
case "5_colored":
case "five_colored":
return getColoredTps(serverUtils.getTps()[1]);
case "15_colored":
case "fifteen_colored":
return getColoredTps(serverUtils.getTps()[2]);
case "percent": {
final StringJoiner joiner = new StringJoiner(ChatColor.GRAY + ", ");
for (double t : serverUtils.getTps()) {
joiner.add(getColoredTpsPercent(t));
}
return joiner.toString();
}
case "1_percent":
case "one_percent":
return getPercent(serverUtils.getTps()[0]);
case "5_percent":
case "five_percent":
return getPercent(serverUtils.getTps()[1]);
case "15_percent":
case "fifteen_percent":
return getPercent(serverUtils.getTps()[2]);
case "1_percent_colored":
case "one_percent_colored":
return getColoredTpsPercent(serverUtils.getTps()[0]);
case "5_percent_colored":
case "five_percent_colored":
return getColoredTpsPercent(serverUtils.getTps()[1]);
case "15_percent_colored":
case "fifteen_percent_colored":
return getColoredTpsPercent(serverUtils.getTps()[2]);
}
return null;
}
/**
* @author Sxtanna
*/
public static String formatTime(final Duration duration) {
final StringBuilder builder = new StringBuilder();
long seconds = duration.getSeconds();
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
final long weeks = days / 7;
seconds %= 60;
minutes %= 60;
hours %= 24;
days %= 7;
if (seconds > 0) {
builder.insert(0, seconds + "s");
}
if (minutes > 0) {
if (builder.length() > 0) {
builder.insert(0, ' ');
}
builder.insert(0, minutes + "m");
}
if (hours > 0) {
if (builder.length() > 0) {
builder.insert(0, ' ');
}
builder.insert(0, hours + "h");
}
if (days > 0) {
if (builder.length() > 0) {
builder.insert(0, ' ');
}
builder.insert(0, days + "d");
}
if (weeks > 0) {
if (builder.length() > 0) {
builder.insert(0, ' ');
}
builder.insert(0, weeks + "w");
}
return builder.toString();
}
private String fix(double tps) {
String finalTps = String.format("%.2f", tps);
return (tps > 20.0 ? "*" : "") + finalTps;
}
private String color(double tps) {
return ChatColor.translateAlternateColorCodes('&', (tps > 18.0) ? high : (tps > 16.0) ? medium : low);
}
private String getColoredTps(double tps) {
return color(tps) + fix(tps);
}
private String getColoredTpsPercent(double tps){
return color(tps) + getPercent(tps);
}
private String getPercent(double tps){
double finalPercent = Math.min(Math.round(100 / 20.0 * tps), 100.0);
return (tps > 20.0 ? "*" : "") + finalPercent + "%";
}
}
@@ -1,145 +0,0 @@
package com.extendedclip.papi.expansion.server;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import org.bukkit.Bukkit;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class ServerUtils {
private String version = null;
private String build = null;
private String variant = null;
private final Map<String, String> variants = new LinkedHashMap<>();
private Object craftServer = null;
private Field tps = null;
private boolean hasTpsMethod = false;
public ServerUtils() {
variants.put("io.papermc.paper.threadedregions.RegionizedServer", "Folia");
variants.put("net.pl3x.purpur.PurpurConfig", "Purpur");
variants.put("gg.airplane.AirplaneConfig", "Airplane");
variants.put("com.tuinity.tuinity.config.TuinityConfig", "Tuinity");
variants.put("io.papermc.paper.configuration.ConfigurationLoaders", "Paper"); // New config location for Paper 1.19+
variants.put("com.destroystokyo.paper.PaperConfig", "Paper"); // Still supported by Paper, but deprecated.
variants.put("org.spigotmc.SpigotConfig", "Spigot");
resolveTPSHandler();
}
public String getServerVariant() {
if (variant != null) {
return variant;
}
for(Map.Entry<String, String> variant : variants.entrySet()) {
try {
Class.forName(variant.getKey());
return (this.variant = variant.getValue());
} catch (ClassNotFoundException ignored) {}
}
return (this.variant = "Unknown");
}
public String getVersion() {
if (version != null) {
return version;
}
return (version = Bukkit.getBukkitVersion().split("-")[0]);
}
public String getBuild() {
if (build != null) {
return build;
}
String[] buildParts = Bukkit.getVersion().split("-");
switch (getServerVariant().toLowerCase(Locale.ROOT)) {
case "spigot":
// TODO: Find out what those variants return.
case "tuinity":
case "airplane":
case "purpur":
return (build = buildParts[0]);
case "paper":
if (buildParts.length >= 3) {
if (buildParts[2].contains(" ")) {
return (build = buildParts[2].substring(0, buildParts[2].indexOf(" ")));
} else {
return (build = buildParts[2]);
}
} else {
return (build = "Unknown");
}
default:
return (build = "Unknown");
}
}
public double[] getTps() {
if (hasTpsMethod) {
return Bukkit.getTPS();
}
if (craftServer == null || tps == null) {
return new double[]{0, 0, 0};
}
try {
return (double[]) tps.get(craftServer);
} catch (IllegalAccessException ignored) {
return new double[]{0, 0, 0};
}
}
private void resolveTPSHandler() {
try {
// If this throws is the server not a fork...
Bukkit.class.getMethod("getTPS");
hasTpsMethod = true;
} catch (NoSuchMethodException ignored) {
final String mcVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
try {
if (getMajorVersion() >= 17) {
craftServer = Class.forName("net.minecraft.server.MinecraftServer")
.getMethod("getServer").invoke(null);
} else {
craftServer = Class.forName("net.minecraft.server." + mcVersion + ".MinecraftServer")
.getMethod("getServer").invoke(null);
}
tps = craftServer.getClass().getField("recentTps");
} catch (Exception ex) {
PlaceholderAPIPlugin.getInstance().getLogger().warning("Could not resolve TPS handling!");
ex.printStackTrace();
}
}
}
private int getMajorVersion() {
final Matcher matcher = Pattern.compile("\\(MC: (\\d)\\.(\\d+)\\.?(\\d+?)?\\)").matcher(Bukkit.getVersion());
if (matcher.find()) {
try{
return Integer.parseInt(matcher.toMatchResult().group(2), 10);
} catch (NumberFormatException ignored) {
return -1;
}
} else {
return -1;
}
}
}