Compare commits
No commits in common. "bffd5a02a938d5afe383cccaefb4a240f884287d" and "7b0793ae38ccbd5d60be66f13574dd6f7b7b8598" have entirely different histories.
bffd5a02a9
...
7b0793ae38
@ -13,70 +13,70 @@ public class ChatConfig {
|
||||
/**
|
||||
* The color used for decoration.
|
||||
*/
|
||||
public static TextColor decorationColor = PandaTheme.CHAT_DECORATION_COLOR;
|
||||
public static TextColor decorationColor = NamedTextColor.YELLOW;
|
||||
|
||||
/**
|
||||
* The character used as a pattern for decoration.
|
||||
*/
|
||||
public static char decorationChar = PandaTheme.CHAT_DECORATION_CHAR;
|
||||
public static char decorationChar = '-';
|
||||
|
||||
/**
|
||||
* The default margin for left and right aligned text.
|
||||
*/
|
||||
public static int nbCharMargin = PandaTheme.CHAT_NB_CHAR_MARGIN;
|
||||
public static int nbCharMargin = 1;
|
||||
|
||||
/**
|
||||
* The color used for successful messages.
|
||||
*/
|
||||
public static TextColor successColor = PandaTheme.CHAT_SUCCESS_COLOR;
|
||||
public static TextColor successColor = NamedTextColor.GREEN;
|
||||
|
||||
/**
|
||||
* The color used for error/failure messages.
|
||||
*/
|
||||
public static TextColor failureColor = PandaTheme.CHAT_FAILURE_COLOR;
|
||||
public static TextColor failureColor = NamedTextColor.RED;
|
||||
|
||||
/**
|
||||
* the color used for informational messages.
|
||||
*/
|
||||
public static TextColor infoColor = PandaTheme.CHAT_INFO_COLOR;
|
||||
public static TextColor infoColor = NamedTextColor.GOLD;
|
||||
|
||||
/**
|
||||
* The color used for warning messages.
|
||||
*/
|
||||
public static TextColor warningColor = PandaTheme.CHAT_WARNING_COLOR;
|
||||
public static TextColor warningColor = NamedTextColor.GOLD;
|
||||
|
||||
/**
|
||||
* The color used to display data in a message.
|
||||
*/
|
||||
public static TextColor dataColor = PandaTheme.CHAT_DATA_COLOR;
|
||||
public static TextColor dataColor = NamedTextColor.GRAY;
|
||||
|
||||
/**
|
||||
* The color used for displayed URLs and clickable URLs.
|
||||
*/
|
||||
public static TextColor urlColor = PandaTheme.CHAT_URL_COLOR;
|
||||
public static TextColor urlColor = NamedTextColor.GREEN;
|
||||
|
||||
/**
|
||||
* The color used for displayed commands and clickable commands.
|
||||
*/
|
||||
public static TextColor commandColor = PandaTheme.CHAT_COMMAND_COLOR;
|
||||
public static TextColor commandColor = NamedTextColor.GRAY;
|
||||
|
||||
/**
|
||||
* The color sued to display a command that is highlighted. For example, the current page in a pagination.
|
||||
*/
|
||||
public static TextColor highlightedCommandColor = PandaTheme.CHAT_COMMAND_HIGHLIGHTED_COLOR;
|
||||
public static TextColor highlightedCommandColor = NamedTextColor.WHITE;
|
||||
|
||||
/**
|
||||
* The color used for broadcasted messages.
|
||||
* It is often used in combination with {@link #prefix}.
|
||||
*/
|
||||
public static TextColor broadcastColor = PandaTheme.CHAT_BROADCAST_COLOR;
|
||||
public static TextColor broadcastColor = NamedTextColor.YELLOW;
|
||||
|
||||
/**
|
||||
* The prefix used for prefixed messages.
|
||||
* It can be a sylized name of the server, like {@code "[Pandacube] "}.
|
||||
* It is often used in combination with {@link #broadcastColor}.
|
||||
*/
|
||||
public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX;
|
||||
public static Supplier<Chat> prefix;
|
||||
|
||||
/**
|
||||
* Gets the width of the configured {@link #prefix}.
|
||||
@ -87,70 +87,4 @@ public class ChatConfig {
|
||||
Chat c;
|
||||
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("] ");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
46
pandalib-paper-players/pom.xml
Normal file
46
pandalib-paper-players/pom.xml
Normal file
@ -0,0 +1,46 @@
|
||||
<?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>
|
@ -7,8 +7,6 @@ import org.bukkit.scoreboard.Team;
|
||||
|
||||
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Represents any player on a paper server, either offline or online.
|
||||
*/
|
||||
@ -96,37 +94,4 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
|
||||
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,7 +3,6 @@ package fr.pandacube.lib.paper.players;
|
||||
import com.destroystokyo.paper.ClientOption;
|
||||
import com.destroystokyo.paper.ClientOption.ChatVisibility;
|
||||
import com.destroystokyo.paper.SkinParts;
|
||||
import fr.pandacube.lib.paper.players.PlayerNonPersistentConfig.Expiration;
|
||||
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
||||
import net.kyori.adventure.audience.MessageType;
|
||||
import net.kyori.adventure.identity.Identified;
|
||||
@ -279,30 +278,4 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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,12 +53,6 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>fr.pandacube.lib</groupId>
|
||||
<artifactId>pandalib-players</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Paper -->
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
@ -70,13 +64,6 @@
|
||||
<artifactId>paper-mojangapi</artifactId>
|
||||
<version>${paper.version}-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Cron expression interpreter -->
|
||||
<dependency>
|
||||
<groupId>ch.eitchnet</groupId>
|
||||
<artifactId>cron</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -1,6 +1,5 @@
|
||||
package fr.pandacube.lib.paper;
|
||||
|
||||
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
public class PandaLibPaper {
|
||||
@ -9,15 +8,8 @@ public class PandaLibPaper {
|
||||
|
||||
public static void init(Plugin plugin) {
|
||||
PandaLibPaper.plugin = plugin;
|
||||
|
||||
PerformanceAnalysisManager.getInstance(); // initialize
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static void disable() {
|
||||
PerformanceAnalysisManager.getInstance().cancelInternalBossBar();
|
||||
}
|
||||
|
||||
public static Plugin getPlugin() {
|
||||
return plugin;
|
||||
|
@ -1,475 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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<>();
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
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)
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
package fr.pandacube.lib.players.standalone;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Represents any player, either offline or online.
|
||||
@ -66,24 +65,6 @@ public interface AbstractOffPlayer {
|
||||
*/
|
||||
String getDisplayName();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* 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,6 +75,7 @@
|
||||
<module>pandalib-paper</module>
|
||||
<module>pandalib-paper-commands</module>
|
||||
<module>pandalib-paper-permissions</module>
|
||||
<module>pandalib-paper-players</module>
|
||||
<module>pandalib-paper-reflect</module>
|
||||
<module>pandalib-permissions</module>
|
||||
<module>pandalib-players</module>
|
||||
|
Loading…
Reference in New Issue
Block a user