Compare commits
11 Commits
7b0793ae38
...
bffd5a02a9
Author | SHA1 | Date | |
---|---|---|---|
bffd5a02a9 | |||
f2d1acd24a | |||
264300ead6 | |||
06815c5c75 | |||
5b20cb4372 | |||
35260ff54c | |||
967e3a99e0 | |||
b427d23dd6 | |||
edea5835ad | |||
fc777e7def | |||
b756b912bd |
@ -13,70 +13,70 @@ public class ChatConfig {
|
|||||||
/**
|
/**
|
||||||
* The color used for decoration.
|
* The color used for decoration.
|
||||||
*/
|
*/
|
||||||
public static TextColor decorationColor = NamedTextColor.YELLOW;
|
public static TextColor decorationColor = PandaTheme.CHAT_DECORATION_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The character used as a pattern for decoration.
|
* The character used as a pattern for decoration.
|
||||||
*/
|
*/
|
||||||
public static char decorationChar = '-';
|
public static char decorationChar = PandaTheme.CHAT_DECORATION_CHAR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default margin for left and right aligned text.
|
* The default margin for left and right aligned text.
|
||||||
*/
|
*/
|
||||||
public static int nbCharMargin = 1;
|
public static int nbCharMargin = PandaTheme.CHAT_NB_CHAR_MARGIN;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for successful messages.
|
* The color used for successful messages.
|
||||||
*/
|
*/
|
||||||
public static TextColor successColor = NamedTextColor.GREEN;
|
public static TextColor successColor = PandaTheme.CHAT_SUCCESS_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for error/failure messages.
|
* The color used for error/failure messages.
|
||||||
*/
|
*/
|
||||||
public static TextColor failureColor = NamedTextColor.RED;
|
public static TextColor failureColor = PandaTheme.CHAT_FAILURE_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the color used for informational messages.
|
* the color used for informational messages.
|
||||||
*/
|
*/
|
||||||
public static TextColor infoColor = NamedTextColor.GOLD;
|
public static TextColor infoColor = PandaTheme.CHAT_INFO_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for warning messages.
|
* The color used for warning messages.
|
||||||
*/
|
*/
|
||||||
public static TextColor warningColor = NamedTextColor.GOLD;
|
public static TextColor warningColor = PandaTheme.CHAT_WARNING_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used to display data in a message.
|
* The color used to display data in a message.
|
||||||
*/
|
*/
|
||||||
public static TextColor dataColor = NamedTextColor.GRAY;
|
public static TextColor dataColor = PandaTheme.CHAT_DATA_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for displayed URLs and clickable URLs.
|
* The color used for displayed URLs and clickable URLs.
|
||||||
*/
|
*/
|
||||||
public static TextColor urlColor = NamedTextColor.GREEN;
|
public static TextColor urlColor = PandaTheme.CHAT_URL_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for displayed commands and clickable commands.
|
* The color used for displayed commands and clickable commands.
|
||||||
*/
|
*/
|
||||||
public static TextColor commandColor = NamedTextColor.GRAY;
|
public static TextColor commandColor = PandaTheme.CHAT_COMMAND_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color sued to display a command that is highlighted. For example, the current page in a pagination.
|
* The color sued to display a command that is highlighted. For example, the current page in a pagination.
|
||||||
*/
|
*/
|
||||||
public static TextColor highlightedCommandColor = NamedTextColor.WHITE;
|
public static TextColor highlightedCommandColor = PandaTheme.CHAT_COMMAND_HIGHLIGHTED_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for broadcasted messages.
|
* The color used for broadcasted messages.
|
||||||
* It is often used in combination with {@link #prefix}.
|
* It is often used in combination with {@link #prefix}.
|
||||||
*/
|
*/
|
||||||
public static TextColor broadcastColor = NamedTextColor.YELLOW;
|
public static TextColor broadcastColor = PandaTheme.CHAT_BROADCAST_COLOR;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The prefix used for prefixed messages.
|
* The prefix used for prefixed messages.
|
||||||
* It can be a sylized name of the server, like {@code "[Pandacube] "}.
|
* It can be a sylized name of the server, like {@code "[Pandacube] "}.
|
||||||
* It is often used in combination with {@link #broadcastColor}.
|
* It is often used in combination with {@link #broadcastColor}.
|
||||||
*/
|
*/
|
||||||
public static Supplier<Chat> prefix;
|
public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the width of the configured {@link #prefix}.
|
* Gets the width of the configured {@link #prefix}.
|
||||||
@ -87,4 +87,70 @@ public class ChatConfig {
|
|||||||
Chat c;
|
Chat c;
|
||||||
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console);
|
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static class PandaTheme {
|
||||||
|
|
||||||
|
public static final TextColor CHAT_GREEN_1_NORMAL = TextColor.fromHexString("#3db849"); // h=126 s=50 l=48
|
||||||
|
public static final TextColor CHAT_GREEN_2 = TextColor.fromHexString("#5ec969"); // h=126 s=50 l=58
|
||||||
|
public static final TextColor CHAT_GREEN_3 = TextColor.fromHexString("#85d68d"); // h=126 s=50 l=68
|
||||||
|
public static final TextColor CHAT_GREEN_4 = TextColor.fromHexString("#abe3b0"); // h=126 s=50 l=78
|
||||||
|
|
||||||
|
public static final TextColor CHAT_GREEN_SATMAX = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50
|
||||||
|
public static final TextColor CHAT_GREEN_1_SAT = TextColor.fromHexString("#20d532"); // h=126 s=50 l=48
|
||||||
|
public static final TextColor CHAT_GREEN_2_SAT = TextColor.fromHexString("#45e354"); // h=126 s=50 l=58
|
||||||
|
public static final TextColor CHAT_GREEN_3_SAT = TextColor.fromHexString("#71ea7d"); // h=126 s=50 l=68
|
||||||
|
public static final TextColor CHAT_GREEN_4_SAT = TextColor.fromHexString("#9df0a6"); // h=126 s=50 l=78
|
||||||
|
|
||||||
|
public static final TextColor CHAT_BROWN_1 = TextColor.fromHexString("#b26d3a"); // h=26 s=51 l=46
|
||||||
|
public static final TextColor CHAT_BROWN_2 = TextColor.fromHexString("#cd9265"); // h=26 s=51 l=60
|
||||||
|
public static final TextColor CHAT_BROWN_3 = TextColor.fromHexString("#e0bb9f"); // h=26 s=51 l=75
|
||||||
|
|
||||||
|
public static final TextColor CHAT_BROWN_1_SAT = TextColor.fromHexString("#b35c19"); // h=26 s=75 l=40
|
||||||
|
public static final TextColor CHAT_BROWN_2_SAT = TextColor.fromHexString("#e28136"); // h=26 s=51 l=55
|
||||||
|
public static final TextColor CHAT_BROWN_3_SAT = TextColor.fromHexString("#ecab79"); // h=26 s=51 l=70
|
||||||
|
|
||||||
|
public static final TextColor CHAT_GRAY_MID = TextColor.fromHexString("#888888");
|
||||||
|
|
||||||
|
public static final TextColor CHAT_BROADCAST_COLOR = NamedTextColor.YELLOW;
|
||||||
|
|
||||||
|
|
||||||
|
public static final TextColor CHAT_DECORATION_COLOR = CHAT_GREEN_1_NORMAL;
|
||||||
|
public static final char CHAT_DECORATION_CHAR = '-';
|
||||||
|
public static final TextColor CHAT_URL_COLOR = CHAT_GREEN_1_NORMAL;
|
||||||
|
public static final TextColor CHAT_COMMAND_COLOR = CHAT_GRAY_MID;
|
||||||
|
public static final TextColor CHAT_COMMAND_HIGHLIGHTED_COLOR = NamedTextColor.WHITE;
|
||||||
|
public static final TextColor CHAT_INFO_COLOR = CHAT_GREEN_4;
|
||||||
|
public static final TextColor CHAT_WARNING_COLOR = CHAT_BROWN_2_SAT;
|
||||||
|
public static final TextColor CHAT_SUCCESS_COLOR = CHAT_GREEN_SATMAX;
|
||||||
|
public static final TextColor CHAT_FAILURE_COLOR = TextColor.fromHexString("#ff3333");
|
||||||
|
public static final TextColor CHAT_DATA_COLOR = CHAT_GRAY_MID;
|
||||||
|
|
||||||
|
|
||||||
|
public static final TextColor CHAT_PM_PREFIX_DECORATION = CHAT_BROWN_2_SAT;
|
||||||
|
public static final TextColor CHAT_PM_SELF_MESSAGE = CHAT_GREEN_2;
|
||||||
|
public static final TextColor CHAT_PM_OTHER_MESSAGE = CHAT_GREEN_4;
|
||||||
|
|
||||||
|
|
||||||
|
public static final TextColor CHAT_DISCORD_LINK_COLOR = TextColor.fromHexString("#00aff4");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of decoration character to put between the text and the border of
|
||||||
|
* the line for left and right aligned text.
|
||||||
|
*/
|
||||||
|
public static final int CHAT_NB_CHAR_MARGIN = 1;
|
||||||
|
|
||||||
|
|
||||||
|
public static Chat CHAT_MESSAGE_PREFIX() {
|
||||||
|
return Chat.text("[")
|
||||||
|
.broadcastColor()
|
||||||
|
.thenDecoration("Serveur")
|
||||||
|
.thenText("] ");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<parent>
|
|
||||||
<artifactId>pandalib-parent</artifactId>
|
|
||||||
<groupId>fr.pandacube.lib</groupId>
|
|
||||||
<version>1.0-SNAPSHOT</version>
|
|
||||||
<relativePath>../pom.xml</relativePath>
|
|
||||||
</parent>
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<artifactId>pandalib-paper-players</artifactId>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>papermc</id>
|
|
||||||
<url>https://papermc.io/repo/repository/maven-public/</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>fr.pandacube.lib</groupId>
|
|
||||||
<artifactId>pandalib-players</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Paper -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.papermc.paper</groupId>
|
|
||||||
<artifactId>paper-api</artifactId>
|
|
||||||
<version>${paper.version}-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.papermc.paper</groupId>
|
|
||||||
<artifactId>paper-mojangapi</artifactId>
|
|
||||||
<version>${paper.version}-SNAPSHOT</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</project>
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package fr.pandacube.lib.paper.reflect.util;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftServer;
|
||||||
|
import fr.pandacube.lib.reflect.wrapper.ReflectWrapper;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to get the list of the primary world of the Bukkit/Paper server.
|
||||||
|
* The primary worlds are the world that are loaded by the server at the startup, that the plugins cannot unload.
|
||||||
|
*/
|
||||||
|
public class PrimaryWorlds {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An unmodifiable list containing the names of the primary worlds of this server instance, in the order they are
|
||||||
|
* loaded. This list can be accessed even if the corresponding worlds are not yet loaded, for instance in the
|
||||||
|
* {@link Plugin#onLoad()} method.
|
||||||
|
*/
|
||||||
|
public static final List<String> PRIMARY_WORLDS;
|
||||||
|
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<String> primaryWorlds = new ArrayList<>(3);
|
||||||
|
|
||||||
|
String world = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class).getServer().getLevelIdName();
|
||||||
|
|
||||||
|
primaryWorlds.add(world);
|
||||||
|
if (Bukkit.getAllowNether()) primaryWorlds.add(world + "_nether");
|
||||||
|
if (Bukkit.getAllowEnd()) primaryWorlds.add(world + "_the_end");
|
||||||
|
|
||||||
|
PRIMARY_WORLDS = Collections.unmodifiableList(primaryWorlds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -53,6 +53,12 @@
|
|||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<artifactId>pandalib-players</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Paper -->
|
<!-- Paper -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.papermc.paper</groupId>
|
<groupId>io.papermc.paper</groupId>
|
||||||
@ -64,6 +70,13 @@
|
|||||||
<artifactId>paper-mojangapi</artifactId>
|
<artifactId>paper-mojangapi</artifactId>
|
||||||
<version>${paper.version}-SNAPSHOT</version>
|
<version>${paper.version}-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Cron expression interpreter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.eitchnet</groupId>
|
||||||
|
<artifactId>cron</artifactId>
|
||||||
|
<version>1.6.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
@ -1,5 +1,6 @@
|
|||||||
package fr.pandacube.lib.paper;
|
package fr.pandacube.lib.paper;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
public class PandaLibPaper {
|
public class PandaLibPaper {
|
||||||
@ -9,6 +10,13 @@ public class PandaLibPaper {
|
|||||||
public static void init(Plugin plugin) {
|
public static void init(Plugin plugin) {
|
||||||
PandaLibPaper.plugin = plugin;
|
PandaLibPaper.plugin = plugin;
|
||||||
|
|
||||||
|
PerformanceAnalysisManager.getInstance(); // initialize
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void disable() {
|
||||||
|
PerformanceAnalysisManager.getInstance().cancelInternalBossBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Plugin getPlugin() {
|
public static Plugin getPlugin() {
|
||||||
|
@ -0,0 +1,475 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules;
|
||||||
|
|
||||||
|
import com.destroystokyo.paper.event.server.ServerTickEndEvent;
|
||||||
|
import com.destroystokyo.paper.event.server.ServerTickStartEvent;
|
||||||
|
import fr.pandacube.lib.chat.Chat;
|
||||||
|
import fr.pandacube.lib.chat.ChatColorGradient;
|
||||||
|
import fr.pandacube.lib.chat.ChatColorUtil;
|
||||||
|
import fr.pandacube.lib.chat.ChatConfig.PandaTheme;
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.paper.players.PaperOffPlayer;
|
||||||
|
import fr.pandacube.lib.paper.players.PaperOnlinePlayer;
|
||||||
|
import fr.pandacube.lib.paper.scheduler.SchedulerUtil;
|
||||||
|
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar;
|
||||||
|
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar.BarUpdater;
|
||||||
|
import fr.pandacube.lib.players.standalone.AbstractPlayerManager;
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import fr.pandacube.lib.util.MemoryUtil;
|
||||||
|
import fr.pandacube.lib.util.MemoryUtil.MemoryUnit;
|
||||||
|
import fr.pandacube.lib.util.TimeUtil;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Color;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Overlay;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.ConsoleCommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.ThreadMXBean;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static fr.pandacube.lib.chat.ChatStatic.chat;
|
||||||
|
import static fr.pandacube.lib.chat.ChatStatic.failureText;
|
||||||
|
import static fr.pandacube.lib.chat.ChatStatic.infoText;
|
||||||
|
import static fr.pandacube.lib.chat.ChatStatic.successText;
|
||||||
|
import static fr.pandacube.lib.chat.ChatStatic.text;
|
||||||
|
|
||||||
|
public class PerformanceAnalysisManager implements Listener {
|
||||||
|
|
||||||
|
private static PerformanceAnalysisManager instance;
|
||||||
|
|
||||||
|
public static synchronized PerformanceAnalysisManager getInstance() {
|
||||||
|
if (instance == null)
|
||||||
|
instance = new PerformanceAnalysisManager();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static final int NB_TICK_HISTORY = 20 * 60 * 60; // 60 secondes;
|
||||||
|
|
||||||
|
private final Plugin plugin = PandaLibPaper.getPlugin();
|
||||||
|
private long firstRecord = 0;
|
||||||
|
|
||||||
|
private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
||||||
|
|
||||||
|
private long tickStartNanoTime = System.nanoTime();
|
||||||
|
private long tickStartCPUTime = 0;
|
||||||
|
private long tickEndNanoTime = System.nanoTime();
|
||||||
|
private long lastInterTPSDuration = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final LinkedList<Long> tpsTimes = new LinkedList<>();
|
||||||
|
private final LinkedList<Long> tpsDurations = new LinkedList<>();
|
||||||
|
private final LinkedList<Long> tpsCPUTimes = new LinkedList<>();
|
||||||
|
private final LinkedList<Long> interTPSDurations = new LinkedList<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final AutoUpdatedBossBar tpsBar;
|
||||||
|
private final AutoUpdatedBossBar memoryBar;
|
||||||
|
private final List<Player> barPlayers = new ArrayList<>();
|
||||||
|
private final List<BossBar> relatedBossBars = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
public final ChatColorGradient tps1sGradient = new ChatColorGradient()
|
||||||
|
.add(0, NamedTextColor.BLACK)
|
||||||
|
.add(1, NamedTextColor.DARK_RED)
|
||||||
|
.add(5, NamedTextColor.RED)
|
||||||
|
.add(10, NamedTextColor.GOLD)
|
||||||
|
.add(14, NamedTextColor.YELLOW)
|
||||||
|
.add(20, PandaTheme.CHAT_DECORATION_COLOR)
|
||||||
|
.add(26, NamedTextColor.BLUE);
|
||||||
|
|
||||||
|
|
||||||
|
public final ChatColorGradient tps10sGradient = new ChatColorGradient()
|
||||||
|
.add(0, NamedTextColor.DARK_RED)
|
||||||
|
.add(5, NamedTextColor.RED)
|
||||||
|
.add(10, NamedTextColor.GOLD)
|
||||||
|
.add(14, NamedTextColor.YELLOW)
|
||||||
|
.add(18, PandaTheme.CHAT_DECORATION_COLOR);
|
||||||
|
|
||||||
|
|
||||||
|
public final ChatColorGradient tps1mGradient = new ChatColorGradient()
|
||||||
|
.add(0, NamedTextColor.DARK_RED)
|
||||||
|
.add(8, NamedTextColor.RED)
|
||||||
|
.add(12, NamedTextColor.GOLD)
|
||||||
|
.add(16, NamedTextColor.YELLOW)
|
||||||
|
.add(20, PandaTheme.CHAT_DECORATION_COLOR);
|
||||||
|
|
||||||
|
public final ChatColorGradient memoryUsageGradient = new ChatColorGradient()
|
||||||
|
.add(.60f, PandaTheme.CHAT_DECORATION_COLOR)
|
||||||
|
.add(.70f, NamedTextColor.YELLOW)
|
||||||
|
.add(.80f, NamedTextColor.GOLD)
|
||||||
|
.add(.90f, NamedTextColor.RED)
|
||||||
|
.add(.95f , NamedTextColor.DARK_RED);
|
||||||
|
|
||||||
|
|
||||||
|
private PerformanceAnalysisManager() {
|
||||||
|
|
||||||
|
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||||
|
|
||||||
|
BossBar bossBar = BossBar.bossBar(text("TPS Serveur"), 0, Color.GREEN, Overlay.NOTCHED_20);
|
||||||
|
tpsBar = new AutoUpdatedBossBar(bossBar, new TPSBossBarUpdater());
|
||||||
|
tpsBar.scheduleUpdateTimeSyncThreadAsync(1000, 100);
|
||||||
|
|
||||||
|
BossBar bossMemBar = BossBar.bossBar(text("Mémoire Serveur"), 0, Color.GREEN, Overlay.NOTCHED_10);
|
||||||
|
memoryBar = new AutoUpdatedBossBar(bossMemBar, new MemoryBossBarUpdater());
|
||||||
|
memoryBar.scheduleUpdateTimeSyncThreadAsync(1000, 100);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean barsContainsPlayer(Player p) {
|
||||||
|
return barPlayers.contains(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addPlayerToBars(Player p) {
|
||||||
|
barPlayers.add(p);
|
||||||
|
p.showBossBar(tpsBar.bar);
|
||||||
|
p.showBossBar(memoryBar.bar);
|
||||||
|
for (BossBar bar : relatedBossBars)
|
||||||
|
p.showBossBar(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void removePlayerToBars(Player p) {
|
||||||
|
p.hideBossBar(tpsBar.bar);
|
||||||
|
p.hideBossBar(memoryBar.bar);
|
||||||
|
for (BossBar bar : relatedBossBars)
|
||||||
|
p.hideBossBar(bar);
|
||||||
|
barPlayers.remove(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addBossBar(BossBar bar) {
|
||||||
|
if (relatedBossBars.contains(bar))
|
||||||
|
return;
|
||||||
|
relatedBossBars.add(bar);
|
||||||
|
for (Player p : barPlayers)
|
||||||
|
p.showBossBar(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void removeBossBar(BossBar bar) {
|
||||||
|
if (!relatedBossBars.contains(bar))
|
||||||
|
return;
|
||||||
|
relatedBossBars.remove(bar);
|
||||||
|
for (Player p : barPlayers)
|
||||||
|
p.hideBossBar(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void cancelInternalBossBar() {
|
||||||
|
tpsBar.cancel();
|
||||||
|
memoryBar.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public synchronized void onTickStart(ServerTickStartEvent event) {
|
||||||
|
tickStartNanoTime = System.nanoTime();
|
||||||
|
tickStartCPUTime = threadMXBean.isThreadCpuTimeSupported() ? threadMXBean.getCurrentThreadCpuTime() : 0;
|
||||||
|
|
||||||
|
lastInterTPSDuration = firstRecord == 0 ? 0 : (tickStartNanoTime - tickEndNanoTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public synchronized void onTickEnd(ServerTickEndEvent event) {
|
||||||
|
tickEndNanoTime = System.nanoTime();
|
||||||
|
long tickEndCPUTime = threadMXBean.isThreadCpuTimeSupported() ? threadMXBean.getCurrentThreadCpuTime() : 0;
|
||||||
|
|
||||||
|
if (firstRecord == 0) firstRecord = System.currentTimeMillis();
|
||||||
|
|
||||||
|
tpsTimes.add(System.currentTimeMillis());
|
||||||
|
tpsDurations.add(tickEndNanoTime - tickStartNanoTime);
|
||||||
|
tpsCPUTimes.add(tickEndCPUTime - tickStartCPUTime);
|
||||||
|
interTPSDurations.add(lastInterTPSDuration);
|
||||||
|
|
||||||
|
while (tpsTimes.size() > NB_TICK_HISTORY + 1)
|
||||||
|
tpsTimes.poll();
|
||||||
|
while (tpsDurations.size() > NB_TICK_HISTORY + 1)
|
||||||
|
tpsDurations.poll();
|
||||||
|
while (tpsCPUTimes.size() > NB_TICK_HISTORY + 1)
|
||||||
|
tpsCPUTimes.poll();
|
||||||
|
while (interTPSDurations.size() > NB_TICK_HISTORY + 1)
|
||||||
|
interTPSDurations.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AbstractPlayerManager<PaperOnlinePlayer, PaperOffPlayer> playerManager = (AbstractPlayerManager<PaperOnlinePlayer, PaperOffPlayer>) AbstractPlayerManager.getInstance();
|
||||||
|
PaperOffPlayer offP = playerManager.getOffline(event.getPlayer().getUniqueId());
|
||||||
|
try {
|
||||||
|
if ("true".equals(offP.getConfig("system.bar", "false"))) {
|
||||||
|
SchedulerUtil.runOnServerThread(() -> addPlayerToBars(event.getPlayer()));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.severe("Cannot get player config", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
removePlayerToBars(event.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final long maxMem = Runtime.getRuntime().maxMemory();
|
||||||
|
|
||||||
|
|
||||||
|
private class MemoryBossBarUpdater implements BarUpdater {
|
||||||
|
@Override
|
||||||
|
public void update(AutoUpdatedBossBar bar) {
|
||||||
|
long allocMem = Runtime.getRuntime().totalMemory();
|
||||||
|
long freeMem = Runtime.getRuntime().freeMemory();
|
||||||
|
long usedMem = allocMem - freeMem;
|
||||||
|
|
||||||
|
double progress = usedMem / (double)maxMem;
|
||||||
|
progress = (progress < 0) ? 0 : (progress > 1) ? 1 : progress;
|
||||||
|
|
||||||
|
Color barColor = (progress >= 0.85) ? Color.RED
|
||||||
|
: (progress >= 0.65) ? Color.YELLOW
|
||||||
|
: Color.GREEN;
|
||||||
|
|
||||||
|
TextColor usedColor = memoryUsageGradient.pickColorAt((float)progress);
|
||||||
|
|
||||||
|
Chat display = infoText("Mémoire : ")
|
||||||
|
.then(text("Util:" + MemoryUtil.humanReadableSize(usedMem, MemoryUnit.MB, false)
|
||||||
|
+ "/" + MemoryUtil.humanReadableSize(maxMem, MemoryUnit.MB, false)
|
||||||
|
)
|
||||||
|
.color(usedColor)
|
||||||
|
)
|
||||||
|
.thenText(" Allouée:" + MemoryUtil.humanReadableSize(allocMem, MemoryUnit.MB, false));
|
||||||
|
|
||||||
|
bar.setColor(barColor);
|
||||||
|
bar.setProgress(progress);
|
||||||
|
bar.setTitle(display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private class TPSBossBarUpdater implements BarUpdater {
|
||||||
|
@Override
|
||||||
|
public void update(AutoUpdatedBossBar bar) {
|
||||||
|
synchronized (PerformanceAnalysisManager.this) {
|
||||||
|
float tps1s = getTPS(1000);
|
||||||
|
|
||||||
|
Color barColor = (tps1s >= 25) ? Color.WHITE
|
||||||
|
: (tps1s >= 12) ? Color.GREEN
|
||||||
|
: (tps1s >= 6) ? Color.YELLOW
|
||||||
|
: Color.RED;
|
||||||
|
double barProgress = Double.isNaN(tps1s) ? 0 : tps1s/20d;
|
||||||
|
|
||||||
|
Chat title;
|
||||||
|
if (alteredTPSTitle != null) {
|
||||||
|
title = infoText("TPS : ").then(alteredTPSTitle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
String tps1sDisp = Double.isNaN(tps1s) ? "N/A" : (Math.round(tps1s)) + "";
|
||||||
|
|
||||||
|
|
||||||
|
int[] tpsHistory = getTPSHistory();
|
||||||
|
|
||||||
|
// keep the legacy text when generating the bar to save space when converting to component
|
||||||
|
StringBuilder s = new StringBuilder();
|
||||||
|
ChatColor prevC = ChatColor.RESET;
|
||||||
|
for (int i = 58; i >= 0; i--) {
|
||||||
|
int t = tpsHistory[i];
|
||||||
|
ChatColor newC = ChatColorUtil.toBungee(tps1sGradient.pickColorAt(t));
|
||||||
|
if (newC != prevC) {
|
||||||
|
s.append(newC);
|
||||||
|
prevC = newC;
|
||||||
|
}
|
||||||
|
s.append("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// tick time measurement
|
||||||
|
Chat timings;
|
||||||
|
int nbTick1s = getTPS1s();
|
||||||
|
if (nbTick1s == 0) {
|
||||||
|
// we have a lag spike, so we need to display the time since lagging
|
||||||
|
long lagDurationSec = System.nanoTime() - tickEndNanoTime;
|
||||||
|
timings = text("(")
|
||||||
|
.thenFailure("lag:" + dispRound10(lagDurationSec / (double) 1_000_000_000) + "s")
|
||||||
|
.thenText(")");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
float avgTickDuration1s = getAvgNano(tpsDurations, nbTick1s)/1_000_000;
|
||||||
|
|
||||||
|
float avgTickCPUTime1s = getAvgNano(tpsCPUTimes, nbTick1s)/1_000_000;
|
||||||
|
TextColor avgTickCPUTime1sColor = (avgTickDuration1s < 46 || avgTickCPUTime1s < 20) ? PandaTheme.CHAT_DECORATION_COLOR
|
||||||
|
: (avgTickCPUTime1s < 30) ? NamedTextColor.YELLOW
|
||||||
|
: (avgTickCPUTime1s < 40) ? NamedTextColor.GOLD
|
||||||
|
: (avgTickCPUTime1s < 50) ? NamedTextColor.RED
|
||||||
|
: NamedTextColor.DARK_RED;
|
||||||
|
|
||||||
|
float avgTickWaitingTime1s = avgTickDuration1s - avgTickCPUTime1s;
|
||||||
|
TextColor avgTickWaitingTime1sColor = (avgTickDuration1s < 46 || avgTickWaitingTime1s < 20) ? PandaTheme.CHAT_DECORATION_COLOR
|
||||||
|
: (avgTickWaitingTime1s < 30) ? NamedTextColor.YELLOW
|
||||||
|
: (avgTickWaitingTime1s < 40) ? NamedTextColor.GOLD
|
||||||
|
: (avgTickWaitingTime1s < 50) ? NamedTextColor.RED
|
||||||
|
: NamedTextColor.DARK_RED;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
float avgInterTickDuration1s = getAvgNano(interTPSDurations, nbTick1s)/1_000_000;
|
||||||
|
TextColor avgInterTickDuration1sColor = (avgInterTickDuration1s > 10) ? PandaTheme.CHAT_DECORATION_COLOR
|
||||||
|
: (avgInterTickDuration1s > 4) ? NamedTextColor.YELLOW
|
||||||
|
: (avgTickDuration1s < 46) ? NamedTextColor.GOLD
|
||||||
|
: NamedTextColor.RED;
|
||||||
|
|
||||||
|
timings = text("(Tr:")
|
||||||
|
.then(text(Math.round(avgTickCPUTime1s) + "ms").color(avgTickCPUTime1sColor))
|
||||||
|
.thenText(" Tw:")
|
||||||
|
.then(text(Math.round(avgTickWaitingTime1s) + "ms").color(avgTickWaitingTime1sColor))
|
||||||
|
.thenText(" S:")
|
||||||
|
.then(text(Math.round(avgInterTickDuration1s) + "ms").color(avgInterTickDuration1sColor))
|
||||||
|
.thenText(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
title = infoText("TPS [")
|
||||||
|
.thenLegacyText(s.toString())
|
||||||
|
.thenText("] ")
|
||||||
|
.then(text(tps1sDisp+"/20 ").color(tps1sGradient.pickColorAt(tps1s)))
|
||||||
|
.then(timings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bar.setTitle(title);
|
||||||
|
bar.setColor(barColor);
|
||||||
|
bar.setProgress(Math.max(0, Math.min(1, barProgress)));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chat alteredTPSTitle = null;
|
||||||
|
|
||||||
|
public synchronized void setAlteredTPSTitle(Chat title) {
|
||||||
|
alteredTPSTitle = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// special case where the getTPS method always returns a whole number when retrieving the TPS for 1 sec
|
||||||
|
public int getTPS1s() {
|
||||||
|
return (int) getTPS(1_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param nbTicks number of ticks when the avg value is computed from history
|
||||||
|
* @return the avg number of TPS in the interval
|
||||||
|
*/
|
||||||
|
public synchronized float getAvgNano(List<Long> data, int nbTicks) {
|
||||||
|
if (data.size() <= 0) return 0;
|
||||||
|
|
||||||
|
if (nbTicks > data.size()) nbTicks = data.size();
|
||||||
|
|
||||||
|
long sum = 0;
|
||||||
|
for (int i = data.size() - nbTicks; i < data.size(); i++)
|
||||||
|
sum += data.get(i);
|
||||||
|
|
||||||
|
return sum / (float) nbTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param nbMillis number of milliseconds when the avg TPS is computed from history
|
||||||
|
* @return the avg number of TPS in the interval
|
||||||
|
*/
|
||||||
|
public synchronized float getTPS(long nbMillis) {
|
||||||
|
if (tpsTimes.size() == 0) return 0;
|
||||||
|
|
||||||
|
long currentMillis = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (currentMillis - nbMillis < firstRecord) nbMillis = currentMillis - firstRecord;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (Long v : tpsTimes) {
|
||||||
|
if (v > currentMillis - nbMillis) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count * (1000 / (float) nbMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized int[] getTPSHistory() {
|
||||||
|
int[] history = new int[60];
|
||||||
|
|
||||||
|
long currentSec = System.currentTimeMillis() / 1000;
|
||||||
|
|
||||||
|
for (Long v : tpsTimes) {
|
||||||
|
int sec = (int) (currentSec - v/1000) - 1;
|
||||||
|
if (sec < 0 || sec >= 60)
|
||||||
|
continue;
|
||||||
|
history[sec]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return history;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void gc(CommandSender sender) {
|
||||||
|
long t1 = System.currentTimeMillis();
|
||||||
|
long alloc1 = Runtime.getRuntime().totalMemory();
|
||||||
|
System.gc();
|
||||||
|
long t2 = System.currentTimeMillis();
|
||||||
|
long alloc2 = Runtime.getRuntime().totalMemory();
|
||||||
|
long released = alloc1 - alloc2;
|
||||||
|
Chat releasedMemoryMessage = released > 0
|
||||||
|
? successText(MemoryUtil.humanReadableSize(released) + " of memory released for the OS.")
|
||||||
|
: released < 0
|
||||||
|
? failureText(MemoryUtil.humanReadableSize(-released) + " of memory taken from the OS.")
|
||||||
|
: chat();
|
||||||
|
|
||||||
|
Chat finalMessage = successText("GC completed in " + TimeUtil.durationToString(t2 - t1, true) + ". ")
|
||||||
|
.then(releasedMemoryMessage);
|
||||||
|
if (sender != null)
|
||||||
|
sender.sendMessage(finalMessage);
|
||||||
|
if (!(sender instanceof ConsoleCommandSender))
|
||||||
|
Log.info(finalMessage.getLegacyText());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String dispRound10(double val) {
|
||||||
|
long v = (long) Math.ceil(val * 10);
|
||||||
|
return "" + (v / 10f);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
|
||||||
|
public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTime>> {
|
||||||
|
|
||||||
|
public static BackupCleaner KEEPING_N_LAST(int n) {
|
||||||
|
return new BackupCleaner() {
|
||||||
|
@Override
|
||||||
|
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> archives) {
|
||||||
|
return archives.descendingSet().stream()
|
||||||
|
.limit(n)
|
||||||
|
.collect(Collectors.toCollection(TreeSet::new));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) {
|
||||||
|
return new BackupCleaner() {
|
||||||
|
@Override
|
||||||
|
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> localDateTimes) {
|
||||||
|
return localDateTimes.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
ldt -> {
|
||||||
|
return ldt.getYear() * 4 + ldt.getMonthValue() / 2;
|
||||||
|
},
|
||||||
|
TreeMap::new,
|
||||||
|
Collectors.minBy(LocalDateTime::compareTo))
|
||||||
|
)
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.collect(Collectors.toCollection(TreeSet::new));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public BackupCleaner merge(BackupCleaner other) {
|
||||||
|
BackupCleaner self = this;
|
||||||
|
return new BackupCleaner() {
|
||||||
|
@Override
|
||||||
|
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> archives) {
|
||||||
|
TreeSet<LocalDateTime> merged = new TreeSet<>();
|
||||||
|
merged.addAll(self.apply(archives));
|
||||||
|
merged.addAll(other.apply(archives));
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void cleanupArchives(File archiveDir) {
|
||||||
|
String[] files = archiveDir.list();
|
||||||
|
|
||||||
|
Log.info("[Backup] Cleaning up backup directory " + archiveDir + "...");
|
||||||
|
|
||||||
|
TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>();
|
||||||
|
|
||||||
|
for (String filename : files) {
|
||||||
|
File file = new File(archiveDir, filename);
|
||||||
|
if (!filename.matches("\\d{8}-\\d{6}\\.zip")) {
|
||||||
|
Log.warning("[Backup] Invalid file in backup directory: " + file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String dateTimeStr = filename.substring(0, filename.length() - 4);
|
||||||
|
LocalDateTime ldt = LocalDateTime.parse(dateTimeStr, CompressProcess.dateFileNameFormatter);
|
||||||
|
|
||||||
|
datedFiles.put(ldt, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeSet<LocalDateTime> keptFiles = apply(new TreeSet<>(datedFiles.keySet()));
|
||||||
|
|
||||||
|
for (Entry<LocalDateTime, File> datedFile : datedFiles.entrySet()) {
|
||||||
|
if (keptFiles.contains(datedFile.getKey()))
|
||||||
|
continue;
|
||||||
|
// datedFile.getValue().delete(); // TODO check if the filtering is ok before actually removing files
|
||||||
|
Log.info("[Backup] Removed expired backup file " + datedFile.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info("[Backup] Backup directory " + archiveDir + " cleaned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BackupConfig {
|
||||||
|
public boolean worldBackupEnabled = true;
|
||||||
|
public boolean workdirBackupEnabled = true;
|
||||||
|
public String scheduling = "0 2 * * 1"; // cron format, here is everyday at 2am
|
||||||
|
public File backupDirectory = null;
|
||||||
|
public BackupCleaner worldBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
||||||
|
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
|
||||||
|
public List<String> workdirIgnoreList = new ArrayList<>();
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import fc.cron.CronExpression;
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.event.world.WorldLoadEvent;
|
||||||
|
import org.bukkit.event.world.WorldSaveEvent;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public class BackupManager implements Runnable, Listener {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Persist persist;
|
||||||
|
|
||||||
|
private final List<CompressProcess> compressQueue = new ArrayList<>();
|
||||||
|
|
||||||
|
private final Set<String> compressWorlds = new HashSet<>();
|
||||||
|
|
||||||
|
/* package */ AtomicReference<CompressProcess> compressRunning = new AtomicReference<>();
|
||||||
|
|
||||||
|
private final Set<String> dirtyForSave = new HashSet<>();
|
||||||
|
|
||||||
|
BackupConfig config;
|
||||||
|
|
||||||
|
public BackupManager(BackupConfig config) {
|
||||||
|
setConfig(config);
|
||||||
|
persist = new Persist(this);
|
||||||
|
|
||||||
|
|
||||||
|
for (final World world : Bukkit.getWorlds()) {
|
||||||
|
initCompressProcess(Type.WORLDS, world.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
initCompressProcess(Type.WORKDIR, null);
|
||||||
|
|
||||||
|
Bukkit.getServer().getScheduler().runTaskTimer(PandaLibPaper.getPlugin(), this, (60 - Calendar.getInstance().get(Calendar.SECOND)) * 20L, 60 * 20L);
|
||||||
|
|
||||||
|
Bukkit.getServer().getPluginManager().registerEvents(this, PandaLibPaper.getPlugin());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfig(BackupConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void onDisable() {
|
||||||
|
|
||||||
|
if (compressRunning.get() != null) {
|
||||||
|
Log.warning("[Backup] Waiting after the end of a backup...");
|
||||||
|
CompressProcess tmp;
|
||||||
|
while ((tmp = compressRunning.get()) != null) {
|
||||||
|
try {
|
||||||
|
tmp.logProgress();
|
||||||
|
// wait 5 seconds between each progress log
|
||||||
|
// but check if the process has ended each .5 seconds
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (compressRunning.get() == null)
|
||||||
|
break;
|
||||||
|
Thread.sleep(500);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) { // could occur because of synchronization errors/interruption/...
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save dirty status of worlds
|
||||||
|
for (String wName : dirtyForSave) {
|
||||||
|
World w = Bukkit.getWorld(wName);
|
||||||
|
if (w != null)
|
||||||
|
persist.updateDirtyStatusAfterSave(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
persist.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCompressProcess(final Type type, final String worldName) {
|
||||||
|
if (!type.backupEnabled(config))
|
||||||
|
return;
|
||||||
|
if (type == Type.WORLDS) {
|
||||||
|
if (compressWorlds.contains(worldName))
|
||||||
|
return;
|
||||||
|
compressWorlds.add(worldName);
|
||||||
|
}
|
||||||
|
CompressProcess process = type == Type.WORLDS ? new CompressWorldProcess(this, worldName) : new CompressWorkdirProcess(this);
|
||||||
|
process.displayDirtynessStatus();
|
||||||
|
compressQueue.add(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
CompressProcess tmp;
|
||||||
|
if ((tmp = compressRunning.get()) != null) {
|
||||||
|
tmp.logProgress();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
compressQueue.sort(null);
|
||||||
|
for (CompressProcess process : compressQueue) {
|
||||||
|
if (System.currentTimeMillis() >= process.getNext() && process.couldRunNow()) {
|
||||||
|
process.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the timestamp (in ms) of when the next compress will run, depending on since when the files to compress are dirty.
|
||||||
|
* @param dirtySince the timestamp in ms since the files are dirty
|
||||||
|
* @return the timestamp in ms when the next compress of the files should be run, or 0 if it is not yet scheduled
|
||||||
|
*/
|
||||||
|
/* package */ long getNextCompress(long dirtySince) {
|
||||||
|
if (dirtySince == -1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
CronExpression parsedScheduling;
|
||||||
|
try {
|
||||||
|
parsedScheduling = new CronExpression(config.scheduling, false);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.severe("Invalid backup scheduling configuration '" + config.scheduling + "'.", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ZonedDateTime ldt = parsedScheduling.nextTimeAfter(ZonedDateTime.from(Instant.ofEpochMilli(dirtySince)));
|
||||||
|
// Log.info("Compress config: " + compressConfig + " - interval: " + interval);
|
||||||
|
|
||||||
|
return ldt.toInstant().toEpochMilli();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onWorldLoad(WorldLoadEvent event) {
|
||||||
|
initCompressProcess(Type.WORLDS, event.getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onWorldSave(WorldSaveEvent event) {
|
||||||
|
if (event.getWorld().getLoadedChunks().length > 0
|
||||||
|
|| dirtyForSave.contains(event.getWorld().getName())) {
|
||||||
|
persist.updateDirtyStatusAfterSave(event.getWorld());
|
||||||
|
dirtyForSave.remove(event.getWorld().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
|
||||||
|
dirtyForSave.add(event.getFrom().getName());
|
||||||
|
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
dirtyForSave.add(event.getPlayer().getWorld().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,200 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.chat.Chat;
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
|
||||||
|
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar;
|
||||||
|
import fr.pandacube.lib.util.FileUtils;
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Color;
|
||||||
|
import net.kyori.adventure.bossbar.BossBar.Overlay;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
|
import java.time.temporal.ChronoField;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
public abstract class CompressProcess implements Comparable<CompressProcess>, Runnable {
|
||||||
|
protected final BackupManager backupManager;
|
||||||
|
public final Type type;
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private ZipCompressor compressor = null;
|
||||||
|
|
||||||
|
protected CompressProcess(BackupManager bm, final Type t, final String n) {
|
||||||
|
backupManager = bm;
|
||||||
|
type = t;
|
||||||
|
name = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final CompressProcess process) {
|
||||||
|
return Long.compare(getNext(), process.getNext());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public abstract BiPredicate<File, String> getFilenameFilter();
|
||||||
|
|
||||||
|
public abstract File getSourceDir();
|
||||||
|
|
||||||
|
protected abstract void onCompressStart();
|
||||||
|
|
||||||
|
protected abstract void onCompressEnd(boolean success);
|
||||||
|
|
||||||
|
protected abstract File getTargetDir();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
backupManager.compressRunning.set(this);
|
||||||
|
|
||||||
|
BiPredicate<File, String> filter = getFilenameFilter();
|
||||||
|
File sourceDir = getSourceDir();
|
||||||
|
|
||||||
|
if (!sourceDir.exists()) {
|
||||||
|
Log.warning(String.format("%% unable to compress %s (check path: %s)", name, sourceDir.getPath()));
|
||||||
|
backupManager.compressRunning.set(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File targetDir = getTargetDir();
|
||||||
|
File target = new File(targetDir, getDateFileName() + ".zip");
|
||||||
|
|
||||||
|
|
||||||
|
BossBar bossBar = BossBar.bossBar(Chat.text("Archivage"), 0, Color.YELLOW, Overlay.NOTCHED_20);
|
||||||
|
AutoUpdatedBossBar auBossBar = new AutoUpdatedBossBar(bossBar, (bar) -> {
|
||||||
|
bar.setTitle(Chat.infoText("Archivage ")
|
||||||
|
.thenData(type + "\\" + name)
|
||||||
|
.thenText(" : ")
|
||||||
|
.then(compressor == null
|
||||||
|
? Chat.text("Démarrage...")
|
||||||
|
: compressor.getState()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
bar.setProgress(compressor == null ? 0 : compressor.getProgress());
|
||||||
|
});
|
||||||
|
auBossBar.scheduleUpdateTimeSyncThreadAsync(100, 100);
|
||||||
|
|
||||||
|
onCompressStart();
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(PandaLibPaper.getPlugin(), () -> {
|
||||||
|
Log.info("[Backup] starting for " + ChatColor.GRAY + type + "\\" + name + ChatColor.RESET + " ...");
|
||||||
|
|
||||||
|
compressor = new ZipCompressor(sourceDir, target, 9, filter);
|
||||||
|
|
||||||
|
PerformanceAnalysisManager.getInstance().addBossBar(bossBar);
|
||||||
|
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
compressor.compress();
|
||||||
|
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
Log.info("[Backup] finished for " + ChatColor.GRAY + type + "\\" + name + ChatColor.RESET);
|
||||||
|
|
||||||
|
backupManager.persist.updateDirtyStatusAfterCompress(type, name);
|
||||||
|
|
||||||
|
displayDirtynessStatus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
type.backupCleaner(backupManager.config).cleanupArchives(targetDir);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.severe(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
Log.severe("[Backup] Failed: " + sourceDir + " -> " + target, e);
|
||||||
|
|
||||||
|
FileUtils.delete(target);
|
||||||
|
if (target.exists())
|
||||||
|
Log.warning("unable to delete: " + target);
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
backupManager.compressRunning.set(null);
|
||||||
|
boolean successF = success;
|
||||||
|
Bukkit.getScheduler().runTask(PandaLibPaper.getPlugin(), () -> onCompressEnd(successF));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
PerformanceAnalysisManager.getInstance().removeBossBar(bossBar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void displayDirtynessStatus() {
|
||||||
|
if (hasNextScheduled() && type == Type.WORLDS) {
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + type + "\\" + name + ChatColor.RESET + " is dirty. Next backup on "
|
||||||
|
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||||
|
}
|
||||||
|
else if (hasNextScheduled()) {
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + type + "\\" + name + ChatColor.RESET + " next backup on "
|
||||||
|
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + type + "\\" + name + ChatColor.RESET + " is clean. Next backup not scheduled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static DateTimeFormatter dateFileNameFormatter = new DateTimeFormatterBuilder()
|
||||||
|
.append(DateTimeFormatter.BASIC_ISO_DATE)
|
||||||
|
.appendLiteral('-')
|
||||||
|
.appendValue(ChronoField.HOUR_OF_DAY, 2) // there is no DateTimeFormatter.BASIC_ISO_TIME
|
||||||
|
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
|
||||||
|
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
|
||||||
|
.toFormatter();
|
||||||
|
|
||||||
|
|
||||||
|
private String getDateFileName() {
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
return dateFileNameFormatter.format(calendar.toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void logProgress() {
|
||||||
|
if (compressor == null)
|
||||||
|
return;
|
||||||
|
Log.info("[Backup] " + ChatColor.GRAY + type + "\\" + name + ChatColor.RESET + ": " + compressor.getState().getLegacyText());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public boolean couldRunNow() {
|
||||||
|
if (!type.backupEnabled(backupManager.config))
|
||||||
|
return false;
|
||||||
|
if (!backupManager.persist.isDirty(type, name))
|
||||||
|
return false;
|
||||||
|
if (getNext() > System.currentTimeMillis())
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public long getNext() {
|
||||||
|
if (!hasNextScheduled())
|
||||||
|
return Long.MAX_VALUE;
|
||||||
|
return backupManager.getNextCompress(backupManager.persist.isDirtySince(type, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNextScheduled() {
|
||||||
|
return backupManager.persist.isDirty(type, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
public class CompressWorkdirProcess extends CompressProcess {
|
||||||
|
|
||||||
|
protected CompressWorkdirProcess(BackupManager bm) {
|
||||||
|
super(bm, Type.WORKDIR, Type.WORKDIR.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public BiPredicate<File, String> getFilenameFilter() {
|
||||||
|
return new SourceFileFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private class SourceFileFilter implements BiPredicate<File, String> {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(File file, String path) {
|
||||||
|
if (globalExcluded(file, path))
|
||||||
|
return false;
|
||||||
|
for (String exclude : backupManager.config.workdirIgnoreList) {
|
||||||
|
if (exclude.startsWith("/")) { // relative to source of workdir
|
||||||
|
if (path.matches(exclude.substring(1)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
String name = path.substring(path.lastIndexOf("/") + 1);
|
||||||
|
if (name.matches(exclude))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean globalExcluded(File file, String path) {
|
||||||
|
if (file.isDirectory() && new File(file, "level.dat").exists())
|
||||||
|
return true;
|
||||||
|
if (new File(getSourceDir(), "logs").equals(file))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getSourceDir() {
|
||||||
|
return new File(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCompressStart() { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCompressEnd(boolean success) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected File getTargetDir() {
|
||||||
|
return new File(backupManager.config.backupDirectory, "workdir");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.paper.util.WorldUtil;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
public class CompressWorldProcess extends CompressProcess {
|
||||||
|
|
||||||
|
private boolean autoSave = true;
|
||||||
|
|
||||||
|
protected CompressWorldProcess(BackupManager bm, final String n) {
|
||||||
|
super(bm, Type.WORLDS, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private World getWorld() {
|
||||||
|
return Bukkit.getWorld(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public BiPredicate<File, String> getFilenameFilter() {
|
||||||
|
return (f, s) -> true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getSourceDir() {
|
||||||
|
return WorldUtil.worldDir(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCompressStart() {
|
||||||
|
World w = getWorld();
|
||||||
|
if (w == null)
|
||||||
|
return;
|
||||||
|
autoSave = w.isAutoSave();
|
||||||
|
w.setAutoSave(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCompressEnd(boolean success) {
|
||||||
|
World w = getWorld();
|
||||||
|
if (w == null)
|
||||||
|
return;
|
||||||
|
w.setAutoSave(autoSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected File getTargetDir() {
|
||||||
|
return new File(backupManager.config.backupDirectory, type.toString() + "/" + name);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.yaml.snakeyaml.error.YAMLException;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
|
||||||
|
public class Persist extends YamlConfiguration {
|
||||||
|
protected final BackupManager backupManager;
|
||||||
|
|
||||||
|
private final File file;
|
||||||
|
|
||||||
|
// private final Set<String> dirtyWorldsSave = new HashSet<>();
|
||||||
|
|
||||||
|
public Persist(BackupManager bm) {
|
||||||
|
file = new File(PandaLibPaper.getPlugin().getDataFolder(), "backup_persist.yml");
|
||||||
|
backupManager = bm;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void load() {
|
||||||
|
boolean loaded = false;
|
||||||
|
try {
|
||||||
|
load(file);
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
catch (final FileNotFoundException ignored) { }
|
||||||
|
catch (final IOException e) {
|
||||||
|
Log.severe("cannot load " + file, e);
|
||||||
|
}
|
||||||
|
catch (final InvalidConfigurationException e) {
|
||||||
|
if (e.getCause() instanceof YAMLException) Log.severe("Config file " + file + " isn't valid!", e);
|
||||||
|
else if (e.getCause() == null || e.getCause() instanceof ClassCastException) Log.severe("Config file " + file + " isn't valid!");
|
||||||
|
else Log.severe("cannot load " + file, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
options().copyDefaults(true);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
try {
|
||||||
|
save(file);
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
Log.severe("could not save " + file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the specified world dirty for compress. Also makes the specified world clean for saving if nobody is connected there.
|
||||||
|
*/
|
||||||
|
public void updateDirtyStatusAfterSave(final World world) {
|
||||||
|
if (world == null)
|
||||||
|
return;
|
||||||
|
if (!isDirty(Type.WORLDS, world.getName())) { // don't set dirty if it is already
|
||||||
|
setDirtySinceNow(Type.WORLDS, world.getName());
|
||||||
|
Log.info("[Backup] " + Type.WORLDS + "\\" + world.getName() + " was saved and is now dirty. Next backup on "
|
||||||
|
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG)
|
||||||
|
.format(new Date(backupManager.getNextCompress(System.currentTimeMillis())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the dirty status after the specified compress process is done.
|
||||||
|
* @param t the type of process
|
||||||
|
* @param n the name of the process (for instance, the name of the world)
|
||||||
|
*/
|
||||||
|
public void updateDirtyStatusAfterCompress(Type t, String n) {
|
||||||
|
if (t == Type.WORLDS)
|
||||||
|
setNotDirty(t, n);
|
||||||
|
else
|
||||||
|
setDirtySinceNow(t, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setDirtySinceNow(Type t, String n) {
|
||||||
|
set(t + "." + n + ".dirty_since", System.currentTimeMillis());
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNotDirty(Type t, String n) {
|
||||||
|
if (t == Type.WORKDIR)
|
||||||
|
return; // WORKDIR are always considered dirty
|
||||||
|
set(t + "." + n + ".dirty_since", -1);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isDirty(Type t, String n) {
|
||||||
|
if (t == Type.WORKDIR)
|
||||||
|
return true;
|
||||||
|
return isDirtySince(t, n) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long isDirtySince(Type t, String n) {
|
||||||
|
if (!contains(t + "." + n + ".dirty_since"))
|
||||||
|
setDirtySinceNow(t, n);
|
||||||
|
return getLong(t + "." + n + ".dirty_since");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* type: // (worlds|others)
|
||||||
|
* name: // (root|plugin|<worldName>)
|
||||||
|
* dirty_since: (true|false)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
WORLDS,
|
||||||
|
WORKDIR;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean backupEnabled(BackupConfig cfg) {
|
||||||
|
return switch (this) {
|
||||||
|
case WORLDS -> cfg.worldBackupEnabled;
|
||||||
|
case WORKDIR -> cfg.workdirBackupEnabled;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackupCleaner backupCleaner(BackupConfig cfg) {
|
||||||
|
return switch (this) {
|
||||||
|
case WORLDS -> cfg.worldBackupCleaner;
|
||||||
|
case WORKDIR -> cfg.workdirBackupCleaner;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
package fr.pandacube.lib.paper.modules.backup;
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.chat.Chat;
|
||||||
|
import fr.pandacube.lib.util.MemoryUtil;
|
||||||
|
import fr.pandacube.lib.util.TimeUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the creation of a zip file that will have the content of a provided folder.
|
||||||
|
*/
|
||||||
|
public class ZipCompressor {
|
||||||
|
private static final int BUFFER_SIZE = 16 * 1024;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final File srcDir, destFile;
|
||||||
|
private final int compressionLevel;
|
||||||
|
private final BiPredicate<File, String> filter;
|
||||||
|
|
||||||
|
private final List<Entry> entriesToCompress;
|
||||||
|
private ZipOutputStream zipOutStream;
|
||||||
|
|
||||||
|
private final Object stateLock = new Object();
|
||||||
|
private final long inputByteSize;
|
||||||
|
private long startTime;
|
||||||
|
private long elapsedByte = 0;
|
||||||
|
private Exception exception = null;
|
||||||
|
private boolean started = false;
|
||||||
|
private boolean finished = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new zip compressor.
|
||||||
|
* @param s the source directory.
|
||||||
|
* @param d the destination file.
|
||||||
|
* @param c the compression level, used in {@link ZipOutputStream#setLevel(int)} .
|
||||||
|
* @param f a filter that returns true for the files to include in the zip file, false to exclude.
|
||||||
|
*/
|
||||||
|
public ZipCompressor(File s, File d, int c, BiPredicate<File, String> f) {
|
||||||
|
srcDir = s;
|
||||||
|
destFile = d;
|
||||||
|
compressionLevel = c;
|
||||||
|
filter = f;
|
||||||
|
|
||||||
|
entriesToCompress = new ArrayList<>();
|
||||||
|
inputByteSize = addEntry("");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a displayable representation of the running compression.
|
||||||
|
* @return a displayable representation of the running compression.
|
||||||
|
*/
|
||||||
|
public Chat getState() {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
if (!started) {
|
||||||
|
return Chat.text("Démarrage...");
|
||||||
|
}
|
||||||
|
else if (!finished && exception == null) {
|
||||||
|
float progress = getProgress();
|
||||||
|
long elapsedTime = System.nanoTime() - startTime;
|
||||||
|
long remainingTime = (long)(elapsedTime / progress) - elapsedTime;
|
||||||
|
return Chat.chat()
|
||||||
|
.infoColor()
|
||||||
|
.thenData(Math.round(progress*100*10)/10 + "% ")
|
||||||
|
.thenText("(")
|
||||||
|
.thenData(MemoryUtil.humanReadableSize(elapsedByte) + "/" + MemoryUtil.humanReadableSize(inputByteSize))
|
||||||
|
.thenText(") - Temps restant estimé : ")
|
||||||
|
.thenData(TimeUtil.durationToString(remainingTime / 1000000));
|
||||||
|
}
|
||||||
|
else if (exception != null) {
|
||||||
|
return Chat.failureText("Erreur lors de l'archivage (voir console pour les détails)");
|
||||||
|
}
|
||||||
|
else { // finished
|
||||||
|
return Chat.successText("Terminé !");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the progress of the running compression.
|
||||||
|
* @return the progress of the running compression 0 when it starts and 1 when it finishes.
|
||||||
|
*/
|
||||||
|
public float getProgress() {
|
||||||
|
if (!started)
|
||||||
|
return 0;
|
||||||
|
if (finished)
|
||||||
|
return 1;
|
||||||
|
return elapsedByte / (float) inputByteSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the compression on the current thread, and returns after the end of the compression.
|
||||||
|
* Should be run asynchronously (not on Server Thread).
|
||||||
|
* @throws Exception if an error occurs during compression.
|
||||||
|
*/
|
||||||
|
public void compress() throws Exception {
|
||||||
|
destFile.getParentFile().mkdirs();
|
||||||
|
|
||||||
|
try(ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destFile), BUFFER_SIZE))) {
|
||||||
|
zipOutStream = zipStream;
|
||||||
|
zipOutStream.setLevel(compressionLevel);
|
||||||
|
|
||||||
|
synchronized (stateLock) {
|
||||||
|
startTime = System.nanoTime();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Entry entry : entriesToCompress) {
|
||||||
|
entry.zip();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (stateLock) {
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
synchronized (stateLock) {
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private long addEntry(String currentEntry) {
|
||||||
|
final File currentFile = new File(srcDir, currentEntry);
|
||||||
|
if (!currentFile.exists())
|
||||||
|
return 0;
|
||||||
|
if (currentFile.isDirectory()) {
|
||||||
|
if (!currentEntry.isEmpty()) { // it's not the zip root directory
|
||||||
|
currentEntry += "/";
|
||||||
|
entriesToCompress.add(new Entry(currentFile, currentEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
long sum = 0;
|
||||||
|
for (String child : currentFile.list()) {
|
||||||
|
String childEntry = currentEntry + child;
|
||||||
|
if (filter.test(new File(currentFile, child), childEntry))
|
||||||
|
sum += addEntry(childEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
else { // is a file
|
||||||
|
entriesToCompress.add(new Entry(currentFile, currentEntry));
|
||||||
|
return currentFile.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Entry {
|
||||||
|
File file;
|
||||||
|
String entry;
|
||||||
|
Entry(File f, String e) {
|
||||||
|
file = f;
|
||||||
|
entry = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
void zip() throws IOException {
|
||||||
|
ZipEntry zipEntry = new ZipEntry(entry);
|
||||||
|
BasicFileAttributes attributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
|
||||||
|
if (attributes.isDirectory()) {
|
||||||
|
zipOutStream.putNextEntry(zipEntry);
|
||||||
|
zipOutStream.closeEntry();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
zipEntry.setTime(attributes.lastModifiedTime().toMillis());
|
||||||
|
zipOutStream.putNextEntry(zipEntry);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.copy(file.toPath(), zipOutStream);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
zipOutStream.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (stateLock) {
|
||||||
|
elapsedByte += attributes.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,8 @@ import org.bukkit.scoreboard.Team;
|
|||||||
|
|
||||||
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
|
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
|
||||||
|
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents any player on a paper server, either offline or online.
|
* Represents any player on a paper server, either offline or online.
|
||||||
*/
|
*/
|
||||||
@ -94,4 +96,37 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
|
|||||||
return teamPrefix + teamColor + name + teamSuffix;
|
return teamPrefix + teamColor + name + teamSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Player config
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default String getConfig(String key) throws Exception {
|
||||||
|
return PaperPlayerConfigStorage.get(getUniqueId(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default String getConfig(String key, String deflt) throws Exception {
|
||||||
|
return PaperPlayerConfigStorage.get(getUniqueId(), key, deflt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void setConfig(String key, String value) throws Exception {
|
||||||
|
PaperPlayerConfigStorage.set(getUniqueId(), key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void updateConfig(String key, String deflt, UnaryOperator<String> updater) throws Exception {
|
||||||
|
PaperPlayerConfigStorage.update(getUniqueId(), key, deflt, updater);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void unsetConfig(String key) throws Exception {
|
||||||
|
PaperPlayerConfigStorage.unset(getUniqueId(), key);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ package fr.pandacube.lib.paper.players;
|
|||||||
import com.destroystokyo.paper.ClientOption;
|
import com.destroystokyo.paper.ClientOption;
|
||||||
import com.destroystokyo.paper.ClientOption.ChatVisibility;
|
import com.destroystokyo.paper.ClientOption.ChatVisibility;
|
||||||
import com.destroystokyo.paper.SkinParts;
|
import com.destroystokyo.paper.SkinParts;
|
||||||
|
import fr.pandacube.lib.paper.players.PlayerNonPersistentConfig.Expiration;
|
||||||
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
||||||
import net.kyori.adventure.audience.MessageType;
|
import net.kyori.adventure.audience.MessageType;
|
||||||
import net.kyori.adventure.identity.Identified;
|
import net.kyori.adventure.identity.Identified;
|
||||||
@ -278,4 +279,30 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Player config
|
||||||
|
*/
|
||||||
|
|
||||||
|
default String getNonPersistentConfig(String key) {
|
||||||
|
return PlayerNonPersistentConfig.getData(getUniqueId(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
default String getNonPersistentConfig(String key, String deflt) {
|
||||||
|
return PlayerNonPersistentConfig.getData(getUniqueId(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setNonPersistentConfig(String key, String value, Expiration expiration) {
|
||||||
|
PlayerNonPersistentConfig.setData(getUniqueId(), key, value, expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void unsetNonPersistentConfig(String key) {
|
||||||
|
PlayerNonPersistentConfig.unsetData(getUniqueId(), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,213 @@
|
|||||||
|
package fr.pandacube.lib.paper.players;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class PaperPlayerConfigStorage {
|
||||||
|
|
||||||
|
static File storageFile = new File(PandaLibPaper.getPlugin().getDataFolder(), "playerdata.yml");
|
||||||
|
static boolean initialized = false;
|
||||||
|
|
||||||
|
static LinkedHashMap<ConfigKey, ConfigEntry> data = new LinkedHashMap<>();
|
||||||
|
static LinkedHashMap<UUID, LinkedHashSet<ConfigEntry>> playerSortedData = new LinkedHashMap<>();
|
||||||
|
static LinkedHashMap<String, LinkedHashSet<ConfigEntry>> keySortedData = new LinkedHashMap<>();
|
||||||
|
static boolean changed = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static synchronized void initIfNeeded() {
|
||||||
|
if (initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
load();
|
||||||
|
} catch (InvalidConfigurationException|IOException e) {
|
||||||
|
throw new RuntimeException("Unable to load the player data file.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-save every 30 seconds
|
||||||
|
Bukkit.getScheduler().runTaskTimerAsynchronously(PandaLibPaper.getPlugin(), PaperPlayerConfigStorage::save, 600, 600);
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static synchronized void load() throws IOException, InvalidConfigurationException {
|
||||||
|
YamlConfiguration config = new YamlConfiguration();
|
||||||
|
config.load(storageFile);
|
||||||
|
data.clear();
|
||||||
|
playerSortedData.clear();
|
||||||
|
keySortedData.clear();
|
||||||
|
for (String pIdStr : config.getKeys(false)) {
|
||||||
|
UUID pId;
|
||||||
|
try {
|
||||||
|
pId = UUID.fromString(pIdStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.severe("Invalid player UUID: '" + pIdStr + "'", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ConfigurationSection sec = config.getConfigurationSection(pIdStr);
|
||||||
|
for (String key : sec.getKeys(false)) {
|
||||||
|
String value = sec.getString(key);
|
||||||
|
create(pId, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static synchronized void save() {
|
||||||
|
YamlConfiguration config = new YamlConfiguration();
|
||||||
|
for (UUID pId : playerSortedData.keySet()) {
|
||||||
|
String pIdStr = pId.toString();
|
||||||
|
ConfigurationSection sec = new YamlConfiguration();
|
||||||
|
for (ConfigEntry e : playerSortedData.get(pId)) {
|
||||||
|
sec.set(e.key, e.value);
|
||||||
|
}
|
||||||
|
config.set(pIdStr, sec);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
config.save(storageFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unable to save the player data file.", e);
|
||||||
|
}
|
||||||
|
changed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static synchronized void create(UUID player, String key, String newValue) {
|
||||||
|
ConfigKey cKey = new ConfigKey(player, key);
|
||||||
|
ConfigEntry e = new ConfigEntry(player, key, newValue);
|
||||||
|
data.put(cKey, e);
|
||||||
|
playerSortedData.computeIfAbsent(player, p -> new LinkedHashSet<>()).add(e);
|
||||||
|
keySortedData.computeIfAbsent(key, p -> new LinkedHashSet<>()).add(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static synchronized void set(UUID player, String key, String newValue) {
|
||||||
|
initIfNeeded();
|
||||||
|
ConfigKey cKey = new ConfigKey(player, key);
|
||||||
|
ConfigEntry e = data.get(cKey);
|
||||||
|
if (e != null && newValue == null) { // delete
|
||||||
|
data.remove(cKey);
|
||||||
|
if (playerSortedData.containsKey(player))
|
||||||
|
playerSortedData.get(player).remove(e);
|
||||||
|
if (keySortedData.containsKey(key))
|
||||||
|
keySortedData.get(key).remove(e);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
else if (e == null && newValue != null) { // create
|
||||||
|
create(player, key, newValue);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
else if (e != null && !newValue.equals(e.value)) { // update
|
||||||
|
e.value = newValue;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized String get(UUID player, String key) {
|
||||||
|
initIfNeeded();
|
||||||
|
ConfigEntry e = data.get(new ConfigKey(player, key));
|
||||||
|
return e != null ? e.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String get(UUID p, String k, String deflt) {
|
||||||
|
String value = get(p, k);
|
||||||
|
return value == null ? deflt : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void update(UUID p, String k, String deflt, UnaryOperator<String> updater) {
|
||||||
|
String oldValue = get(p, k, deflt);
|
||||||
|
set(p, k, updater.apply(oldValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unset(UUID p, String k) {
|
||||||
|
set(p, k, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static LinkedHashSet<ConfigEntry> getAllFromPlayer(UUID p) {
|
||||||
|
initIfNeeded();
|
||||||
|
return new LinkedHashSet<>(playerSortedData.getOrDefault(p, new LinkedHashSet<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinkedHashSet<ConfigEntry> getAllWithKeys(String key) {
|
||||||
|
initIfNeeded();
|
||||||
|
return new LinkedHashSet<>(keySortedData.getOrDefault(key, new LinkedHashSet<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinkedHashSet<ConfigEntry> getAllWithKeyValue(String k, String v) {
|
||||||
|
initIfNeeded();
|
||||||
|
return getAllWithKeys(k).stream()
|
||||||
|
.filter(c -> c.value.equals(v))
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private record ConfigKey(UUID playerId, String key) { }
|
||||||
|
|
||||||
|
public static class ConfigEntry {
|
||||||
|
private final UUID playerId;
|
||||||
|
private final String key;
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
private ConfigEntry(UUID playerId, String key, String value) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(playerId, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return obj instanceof ConfigEntry o
|
||||||
|
&& Objects.equals(playerId, o.playerId)
|
||||||
|
&& Objects.equals(key, o.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
package fr.pandacube.lib.paper.players;
|
||||||
|
|
||||||
|
import com.destroystokyo.paper.event.server.ServerTickStartEvent;
|
||||||
|
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PlayerNonPersistentConfig {
|
||||||
|
private static final Map<UUID, Map<String, ConfigEntry>> data = new HashMap<>();
|
||||||
|
|
||||||
|
private static long tick = 0;
|
||||||
|
|
||||||
|
static {
|
||||||
|
new ConfigListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void setData(UUID playerId, String key, String value, Expiration expiration) {
|
||||||
|
data.computeIfAbsent(Objects.requireNonNull(playerId, "playerId"), pp -> new HashMap<>())
|
||||||
|
.put(Objects.requireNonNull(key, "key"),
|
||||||
|
new ConfigEntry(Objects.requireNonNull(value, "value"),
|
||||||
|
Objects.requireNonNull(expiration, "expiration")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unsetData(UUID playerId, String key) {
|
||||||
|
data.getOrDefault(Objects.requireNonNull(playerId, "playerId"), new HashMap<>())
|
||||||
|
.remove(Objects.requireNonNull(key, "key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getData(UUID playerId, String key) {
|
||||||
|
Map<String, ConfigEntry> playerData = data.getOrDefault(Objects.requireNonNull(playerId, "playerId"), new HashMap<>());
|
||||||
|
ConfigEntry ce = playerData.get(Objects.requireNonNull(key, "key"));
|
||||||
|
if (ce == null)
|
||||||
|
return null;
|
||||||
|
if (!ce.expiration.valid(playerId, key)) {
|
||||||
|
playerData.remove(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ce.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDataSet(UUID playerId, String key) {
|
||||||
|
return getData(playerId, key) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private record ConfigEntry(String value, Expiration expiration) { }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static abstract class Expiration {
|
||||||
|
abstract boolean valid(UUID player, String key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ExpiresLogout extends Expiration {
|
||||||
|
protected boolean valid(UUID player, String key) {
|
||||||
|
return Bukkit.getPlayer(player) != null; // should not be call if player reconnects because it is removed on player quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ExpiresTick extends Expiration {
|
||||||
|
long expirationTick;
|
||||||
|
|
||||||
|
public ExpiresTick(long expirationDelayTick) {
|
||||||
|
expirationTick = tick + expirationDelayTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean valid(UUID player, String key) {
|
||||||
|
return tick < expirationTick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ExpiresServerStop extends Expiration {
|
||||||
|
protected boolean valid(UUID player, String key) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static class ConfigListeners implements Listener {
|
||||||
|
public ConfigListeners() {
|
||||||
|
Bukkit.getPluginManager().registerEvents(this, PandaLibPaper.getPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
data.getOrDefault(event.getPlayer().getUniqueId(), new HashMap<>())
|
||||||
|
.entrySet()
|
||||||
|
.removeIf(e -> e.getValue().expiration instanceof ExpiresLogout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onTickStart(ServerTickStartEvent event) {
|
||||||
|
tick++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package fr.pandacube.lib.paper.util;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World.Environment;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class WorldUtil {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static Environment determineEnvironment(String world) {
|
||||||
|
if (Bukkit.getWorld(world) != null) {
|
||||||
|
return Bukkit.getWorld(world).getEnvironment();
|
||||||
|
}
|
||||||
|
|
||||||
|
File worldFolder = worldDir(world);
|
||||||
|
|
||||||
|
if (!worldFolder.isDirectory())
|
||||||
|
throw new IllegalStateException("The world " + world + " is not a valid world (directory not found).");
|
||||||
|
|
||||||
|
if (!new File(worldFolder, "level.dat").isFile())
|
||||||
|
throw new IllegalStateException("The world " + world + " is not a valid world (level.dat not found).");
|
||||||
|
|
||||||
|
if (new File(worldFolder, "region").isDirectory())
|
||||||
|
return Environment.NORMAL;
|
||||||
|
|
||||||
|
if (new File(worldFolder, "DIM-1" + File.pathSeparator + "region").isDirectory())
|
||||||
|
return Environment.NETHER;
|
||||||
|
|
||||||
|
if (new File(worldFolder, "DIM1" + File.pathSeparator + "region").isDirectory())
|
||||||
|
return Environment.THE_END;
|
||||||
|
|
||||||
|
throw new IllegalStateException("Unable to determine the type of the world " + world + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static final List<String> REGION_DATA_FILES = Arrays.asList("entities", "poi", "region", "DIM-1", "DIM1");
|
||||||
|
|
||||||
|
public static List<File> regionDataFiles(String world) {
|
||||||
|
return onlyExistents(worldDir(world), REGION_DATA_FILES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<File> mapFiles(String world) {
|
||||||
|
Pattern mapFilePattern = Pattern.compile("map_\\d+.dat");
|
||||||
|
return List.of(dataDir(world).listFiles((dir, name) -> mapFilePattern.matcher(name).find() || "idcounts.dat".equals(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<File> onlyExistents(File worldDir, List<String> searchList) {
|
||||||
|
return searchList.stream()
|
||||||
|
.map(f -> new File(worldDir, f))
|
||||||
|
.filter(File::exists)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File worldDir(String world) {
|
||||||
|
return new File(Bukkit.getWorldContainer(), world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File dataDir(String world) {
|
||||||
|
return new File(worldDir(world), "data");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isValidWorld(String world) {
|
||||||
|
File d = worldDir(world);
|
||||||
|
return d.isDirectory() && new File(d, "level.dat").isFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package fr.pandacube.lib.players.standalone;
|
package fr.pandacube.lib.players.standalone;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents any player, either offline or online.
|
* Represents any player, either offline or online.
|
||||||
@ -69,6 +70,24 @@ public interface AbstractOffPlayer {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Player config
|
||||||
|
*/
|
||||||
|
|
||||||
|
String getConfig(String key) throws Exception;
|
||||||
|
|
||||||
|
String getConfig(String key, String deflt) throws Exception;
|
||||||
|
|
||||||
|
void setConfig(String key, String value) throws Exception;
|
||||||
|
|
||||||
|
void updateConfig(String key, String deflt, UnaryOperator<String> updater) throws Exception;
|
||||||
|
|
||||||
|
void unsetConfig(String key) throws Exception;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
1
pom.xml
1
pom.xml
@ -75,7 +75,6 @@
|
|||||||
<module>pandalib-paper</module>
|
<module>pandalib-paper</module>
|
||||||
<module>pandalib-paper-commands</module>
|
<module>pandalib-paper-commands</module>
|
||||||
<module>pandalib-paper-permissions</module>
|
<module>pandalib-paper-permissions</module>
|
||||||
<module>pandalib-paper-players</module>
|
|
||||||
<module>pandalib-paper-reflect</module>
|
<module>pandalib-paper-reflect</module>
|
||||||
<module>pandalib-permissions</module>
|
<module>pandalib-permissions</module>
|
||||||
<module>pandalib-players</module>
|
<module>pandalib-players</module>
|
||||||
|
Loading…
Reference in New Issue
Block a user