Update from Git Manager GUI
This commit is contained in:
316
src/main/java/de/viper/autoworldreset/AutoWorldReset.java
Normal file
316
src/main/java/de/viper/autoworldreset/AutoWorldReset.java
Normal file
@@ -0,0 +1,316 @@
|
||||
package de.viper.autoworldreset;
|
||||
|
||||
import de.viper.autoworldreset.scheduler.ResetScheduler;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Consumer;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class AutoWorldReset extends JavaPlugin {
|
||||
|
||||
private ResetManager resetManager;
|
||||
private ResetScheduler scheduler;
|
||||
|
||||
// BUG FIX #9: lang.yml wird jetzt als eigene FileConfiguration geladen und
|
||||
// tatsächlich verwendet. Vorher wurde die Datei zwar gespeichert, aber
|
||||
// Nachrichten wurden immer aus config.yml gelesen – lang.yml war toter Code.
|
||||
private FileConfiguration langConfig;
|
||||
|
||||
private Metrics metrics;
|
||||
private String latestVersion = null;
|
||||
private static final int RESOURCE_ID = 127822;
|
||||
private static final int BSTATS_PLUGIN_ID = 26874;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// BUG FIX #10: registerEvents(this, this) wurde entfernt.
|
||||
// Die Hauptklasse implementierte kein Listener-Interface und hatte keine
|
||||
// @EventHandler-Methoden mehr – das war also eine sinnlose Registrierung,
|
||||
// die zu einer misleadenden Warnung führen kann.
|
||||
getServer().getPluginManager().registerEvents(new UpdateNotifyListener(), this);
|
||||
|
||||
saveDefaultConfig();
|
||||
loadLangConfig(); // lang.yml laden
|
||||
|
||||
resetManager = new ResetManager(this);
|
||||
scheduler = new ResetScheduler(this, resetManager);
|
||||
|
||||
boolean autoReset = getConfig().getBoolean("auto-reset-on-startup", false);
|
||||
if (autoReset) {
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> {
|
||||
String worldName = getConfig().getString("world-name");
|
||||
// BUG FIX #11: Null-Check für worldName beim Startup-Reset.
|
||||
if (worldName == null || worldName.isEmpty()) {
|
||||
getLogger().warning("auto-reset-on-startup ist aktiv, aber 'world-name' ist nicht konfiguriert!");
|
||||
return;
|
||||
}
|
||||
boolean success = resetManager.restoreBackup(worldName);
|
||||
if (success) {
|
||||
getLogger().info("Backup beim Serverstart erfolgreich wiederhergestellt.");
|
||||
} else {
|
||||
getLogger().warning("Backup konnte beim Serverstart nicht wiederhergestellt werden.");
|
||||
}
|
||||
}, 20L * 10);
|
||||
}
|
||||
|
||||
if (getConfig().getBoolean("scheduler.enabled")) {
|
||||
scheduler.start();
|
||||
}
|
||||
|
||||
metrics = new Metrics(this, BSTATS_PLUGIN_ID);
|
||||
getLogger().info("bStats initialisiert.");
|
||||
|
||||
getLatestVersion(latest -> {
|
||||
if (latest == null || latest.isEmpty()) return; // BUG FIX #12: kein weiterer Code bei leerem Ergebnis
|
||||
|
||||
String current = getDescription().getVersion();
|
||||
String normalizedLatest = latest.replaceFirst("(?i)^v\\.?\\s*", "").trim();
|
||||
String normalizedCurrent = current.replaceFirst("(?i)^v\\.?\\s*", "").trim();
|
||||
|
||||
if (isNewerVersion(normalizedLatest, normalizedCurrent)) {
|
||||
latestVersion = latest;
|
||||
getLogger().info("Neue Version verfügbar: " + latest + " (aktuell: " + current + ")");
|
||||
getLogger().info("Download: https://www.spigotmc.org/resources/" + RESOURCE_ID + "/");
|
||||
|
||||
// Bukkit-API-Calls müssen auf dem Hauptthread laufen!
|
||||
// BUG FIX #13: Spielerbenachrichtigung in runTask() verschoben,
|
||||
// da dieser Code in einem Async-Thread läuft (getServer().getScheduler()
|
||||
// .runTaskAsynchronously) und Bukkit-API nicht thread-safe ist.
|
||||
Bukkit.getScheduler().runTask(this, () -> {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.isOp()) {
|
||||
notifyUpdateToPlayer(player, latest, current);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
getLogger().info("AutoWorldReset wurde aktiviert.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (scheduler != null) {
|
||||
scheduler.stop();
|
||||
}
|
||||
getLogger().info("AutoWorldReset wurde deaktiviert.");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// lang.yml Handling
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void loadLangConfig() {
|
||||
File langFile = new File(getDataFolder(), "lang.yml");
|
||||
|
||||
if (!langFile.exists()) {
|
||||
// lang.yml ist nicht in der JAR eingebettet → manuell mit Standardwerten anlegen
|
||||
getDataFolder().mkdirs();
|
||||
YamlConfiguration defaults = new YamlConfiguration();
|
||||
defaults.set("messages.resetting", "&eDie Welt wird zurückgesetzt...");
|
||||
defaults.set("messages.finished", "&aWelt wurde erfolgreich zurückgesetzt!");
|
||||
defaults.set("messages.no_permission", "&cDu hast keine Berechtigung, diesen Befehl auszuführen.");
|
||||
defaults.set("messages.invalid_command", "&cUngültiger Befehl oder Argument.");
|
||||
defaults.set("messages.kick-message", "&cDie Welt wird zurückgesetzt, du wurdest gekickt.");
|
||||
defaults.set("messages.teleport-message","&cDie Welt wird zurückgesetzt. Du wurdest sicher teleportiert.");
|
||||
try {
|
||||
defaults.save(langFile);
|
||||
getLogger().info("lang.yml wurde automatisch erstellt.");
|
||||
} catch (IOException e) {
|
||||
getLogger().warning("lang.yml konnte nicht erstellt werden: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
langConfig = YamlConfiguration.loadConfiguration(langFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest eine Nachricht aus lang.yml. Fällt auf config.yml und dann auf den
|
||||
* Standardwert zurück, wenn der Schlüssel nicht gefunden wird.
|
||||
*/
|
||||
public String getLangMessage(String key, String defaultValue) {
|
||||
if (langConfig != null && langConfig.contains("messages." + key)) {
|
||||
return langConfig.getString("messages." + key, defaultValue);
|
||||
}
|
||||
return getConfig().getString("messages." + key, defaultValue);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Update-Checker
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void getLatestVersion(Consumer<String> consumer) {
|
||||
getServer().getScheduler().runTaskAsynchronously(this, () -> {
|
||||
try {
|
||||
HttpURLConnection connection = (HttpURLConnection)
|
||||
new URL("https://api.spiget.org/v2/resources/" + RESOURCE_ID + "/versions/latest")
|
||||
.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.addRequestProperty("User-Agent", "AutoWorldReset-UpdateChecker/1.0");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
try (Scanner scanner = new Scanner(connection.getInputStream())) {
|
||||
String response = scanner.useDelimiter("\\A").next();
|
||||
JSONObject json = new JSONObject(response);
|
||||
String versionName = json.optString("name", "").trim();
|
||||
consumer.accept(versionName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Update-Check fehlgeschlagen: " + e.getMessage());
|
||||
consumer.accept("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isNewerVersion(String latest, String current) {
|
||||
try {
|
||||
String[] latestParts = latest.split("\\.");
|
||||
String[] currentParts = current.split("\\.");
|
||||
int length = Math.max(latestParts.length, currentParts.length);
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
int latestPart = (i < latestParts.length) ? Integer.parseInt(latestParts[i]) : 0;
|
||||
int currentPart = (i < currentParts.length) ? Integer.parseInt(currentParts[i]) : 0;
|
||||
if (latestPart > currentPart) return true;
|
||||
if (latestPart < currentPart) return false;
|
||||
}
|
||||
return false;
|
||||
} catch (NumberFormatException e) {
|
||||
return !latest.equalsIgnoreCase(current);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyUpdateToPlayer(Player player, String latest, String current) {
|
||||
player.sendMessage("§aEine neue Version von §e" + getDescription().getName()
|
||||
+ " §aist verfügbar: §e" + latest + " §7(aktuell: " + current + ")");
|
||||
player.sendMessage("§eDownload: §bhttps://www.spigotmc.org/resources/" + RESOURCE_ID + "/");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Inner class: Update-Benachrichtigung beim Join
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public class UpdateNotifyListener implements Listener {
|
||||
@EventHandler
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player.isOp() && latestVersion != null) {
|
||||
notifyUpdateToPlayer(player, latestVersion, getDescription().getVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Commands
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!sender.hasPermission("autoworldreset.use")) {
|
||||
sender.sendMessage(color(getLangMessage("no_permission", "&cDu hast keine Berechtigung.")));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!command.getName().equalsIgnoreCase("autoworldreset")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.length == 0) {
|
||||
sender.sendMessage("§eBenutze: /autoworldreset <reset|backup|restore|reload|start|stop|status>");
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "reset":
|
||||
sender.sendMessage(color(getLangMessage("resetting", "&eDie Welt wird zurückgesetzt...")));
|
||||
Bukkit.getScheduler().runTask(this, () -> resetManager.resetWorld());
|
||||
break;
|
||||
|
||||
case "backup":
|
||||
sender.sendMessage("§eBackup wird erstellt...");
|
||||
Bukkit.getScheduler().runTask(this, () -> {
|
||||
boolean success = resetManager.createBackup();
|
||||
sender.sendMessage(success
|
||||
? "§aBackup erfolgreich erstellt."
|
||||
: "§cBackup konnte nicht erstellt werden. Siehe Konsole für Details.");
|
||||
});
|
||||
break;
|
||||
|
||||
case "restore":
|
||||
sender.sendMessage("§eBackup wird wiederhergestellt...");
|
||||
Bukkit.getScheduler().runTask(this, () -> {
|
||||
String worldName = getConfig().getString("world-name");
|
||||
boolean success = resetManager.restoreBackup(worldName);
|
||||
sender.sendMessage(success
|
||||
? "§aBackup erfolgreich wiederhergestellt."
|
||||
: "§cBackup konnte nicht wiederhergestellt werden. Siehe Konsole für Details.");
|
||||
});
|
||||
break;
|
||||
|
||||
case "reload":
|
||||
reloadConfig();
|
||||
loadLangConfig();
|
||||
resetManager = new ResetManager(this);
|
||||
if (scheduler != null) scheduler.stop();
|
||||
scheduler = new ResetScheduler(this, resetManager);
|
||||
if (getConfig().getBoolean("scheduler.enabled")) {
|
||||
scheduler.start();
|
||||
}
|
||||
sender.sendMessage("§aKonfiguration erfolgreich neu geladen.");
|
||||
getLogger().info(sender.getName() + " hat die Konfiguration neu geladen.");
|
||||
break;
|
||||
|
||||
case "start":
|
||||
if (scheduler.isRunning()) {
|
||||
sender.sendMessage("§eScheduler läuft bereits.");
|
||||
} else {
|
||||
scheduler.start();
|
||||
sender.sendMessage("§aScheduler gestartet.");
|
||||
}
|
||||
break;
|
||||
|
||||
case "stop":
|
||||
scheduler.stop();
|
||||
sender.sendMessage("§cScheduler gestoppt.");
|
||||
break;
|
||||
|
||||
// BUG FIX #14: Neuer "status"-Befehl zeigt, ob der Scheduler läuft.
|
||||
case "status":
|
||||
sender.sendMessage("§eScheduler: " + (scheduler.isRunning() ? "§aAktiv" : "§cInaktiv"));
|
||||
sender.sendMessage("§eWelt: §f" + getConfig().getString("world-name", "nicht konfiguriert"));
|
||||
sender.sendMessage("§eCron: §f" + getConfig().getString("scheduler.cron", "-"));
|
||||
break;
|
||||
|
||||
default:
|
||||
sender.sendMessage(color(getLangMessage("invalid_command", "&cUngültiger Befehl oder Argument.")));
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public ResetManager getResetManager() {
|
||||
return resetManager;
|
||||
}
|
||||
|
||||
private String color(String msg) {
|
||||
return ChatColor.translateAlternateColorCodes('&', msg);
|
||||
}
|
||||
}
|
||||
204
src/main/java/de/viper/autoworldreset/ResetManager.java
Normal file
204
src/main/java/de/viper/autoworldreset/ResetManager.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package de.viper.autoworldreset;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ResetManager {
|
||||
|
||||
private final AutoWorldReset plugin;
|
||||
|
||||
public ResetManager(AutoWorldReset plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void resetWorld() {
|
||||
String worldName = plugin.getConfig().getString("world-name");
|
||||
|
||||
if (worldName == null || worldName.isEmpty()) {
|
||||
plugin.getLogger().warning("Keine Welt in der Config angegeben (key: world-name).");
|
||||
return;
|
||||
}
|
||||
|
||||
// BUG FIX #6: Die Backup-Prüfung wurde aus resetWorld() entfernt und korrekt
|
||||
// platziert. Zuvor wurde backup.enabled geprüft, um einen RESTORE abzubrechen –
|
||||
// das ist logisch falsch. Das Flag sollte nur das automatische Erstellen eines
|
||||
// Backups steuern, nicht das Wiederherstellen. Ein Reset setzt immer das Backup
|
||||
// zurück; wenn keins da ist, schlägt restoreBackup() mit einer Warnung fehl.
|
||||
boolean success = restoreBackup(worldName);
|
||||
if (!success) {
|
||||
plugin.getLogger().warning("Reset fehlgeschlagen: Backup konnte nicht wiederhergestellt werden.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean createBackup() {
|
||||
String worldName = plugin.getConfig().getString("world-name");
|
||||
if (worldName == null || worldName.isEmpty()) {
|
||||
plugin.getLogger().warning("Kein Weltname konfiguriert (key: world-name).");
|
||||
return false;
|
||||
}
|
||||
|
||||
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
|
||||
File backupFolder = new File(Bukkit.getWorldContainer(),
|
||||
worldName + "_" + plugin.getConfig().getString("backup.folder-name", "backup"));
|
||||
|
||||
if (!worldFolder.exists()) {
|
||||
plugin.getLogger().warning("Weltordner nicht gefunden: " + worldFolder.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
if (world != null) {
|
||||
handlePlayers(world);
|
||||
world.save();
|
||||
}
|
||||
|
||||
if (backupFolder.exists()) {
|
||||
deleteFolder(backupFolder.toPath());
|
||||
}
|
||||
|
||||
// BUG FIX #7: copyFolder wirft jetzt korrekt eine IOException nach oben.
|
||||
// Vorher wurden Fehler im Stream-Lambda stillschweigend geschluckt, sodass
|
||||
// ein unvollständiges Backup als "erfolgreich" gemeldet wurde.
|
||||
copyFolder(worldFolder.toPath(), backupFolder.toPath());
|
||||
plugin.getLogger().info("Backup der Welt '" + worldName + "' erfolgreich erstellt.");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("Fehler beim Erstellen des Backups: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean restoreBackup(String worldName) {
|
||||
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
|
||||
File backupFolder = new File(Bukkit.getWorldContainer(),
|
||||
worldName + "_" + plugin.getConfig().getString("backup.folder-name", "backup"));
|
||||
|
||||
if (!backupFolder.exists()) {
|
||||
plugin.getLogger().warning("Backup-Ordner existiert nicht: " + backupFolder.getAbsolutePath()
|
||||
+ ". Bitte zuerst ein Backup mit '/awr backup' erstellen.");
|
||||
return false;
|
||||
}
|
||||
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
if (world != null) {
|
||||
handlePlayers(world);
|
||||
world.save();
|
||||
if (!Bukkit.unloadWorld(world, false)) {
|
||||
plugin.getLogger().warning("Welt konnte nicht entladen werden: " + worldName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (worldFolder.exists()) {
|
||||
deleteFolder(worldFolder.toPath());
|
||||
}
|
||||
copyFolder(backupFolder.toPath(), worldFolder.toPath());
|
||||
|
||||
// BUG FIX #8: Rückgabewert von createWorld() prüfen.
|
||||
// Früher wurde immer true zurückgegeben, auch wenn die Welt nicht
|
||||
// geladen werden konnte (createWorld gibt null zurück bei Fehler).
|
||||
World newWorld = Bukkit.createWorld(new WorldCreator(worldName));
|
||||
if (newWorld == null) {
|
||||
plugin.getLogger().severe("Welt '" + worldName + "' konnte nach Restore nicht geladen werden!");
|
||||
return false;
|
||||
}
|
||||
|
||||
plugin.getLogger().info("Backup der Welt '" + worldName + "' erfolgreich wiederhergestellt.");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe("Fehler beim Wiederherstellen: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePlayers(World world) {
|
||||
String mode = plugin.getConfig().getString("handle-players", "TELEPORT").toUpperCase();
|
||||
World fallback = Bukkit.getWorlds().get(0);
|
||||
|
||||
for (Player player : world.getPlayers()) {
|
||||
switch (mode) {
|
||||
case "KICK":
|
||||
// Nachricht aus lang.yml lesen statt hardcoded
|
||||
String kickMsg = plugin.getLangMessage("kick-message", "&cDie Welt wird zurückgesetzt.");
|
||||
player.kickPlayer(org.bukkit.ChatColor.translateAlternateColorCodes('&', kickMsg));
|
||||
break;
|
||||
case "TELEPORT":
|
||||
player.teleport(fallback.getSpawnLocation());
|
||||
player.sendMessage(org.bukkit.ChatColor.translateAlternateColorCodes('&',
|
||||
plugin.getLangMessage("teleport-message",
|
||||
"&cDie Welt wird zurückgesetzt. Du wurdest sicher teleportiert.")));
|
||||
break;
|
||||
case "TELEPORT_BACK":
|
||||
player.teleport(fallback.getSpawnLocation());
|
||||
player.sendMessage(org.bukkit.ChatColor.translateAlternateColorCodes('&',
|
||||
plugin.getLangMessage("teleport-message",
|
||||
"&cDie Welt wird zurückgesetzt. Du wurdest vorübergehend teleportiert.")));
|
||||
break;
|
||||
default:
|
||||
plugin.getLogger().warning("Unbekannter handle-players Modus: '" + mode + "'. Verwende TELEPORT.");
|
||||
player.teleport(fallback.getSpawnLocation());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFolder(Path path) throws IOException {
|
||||
if (Files.notExists(path)) return;
|
||||
if (Files.isDirectory(path)) {
|
||||
try (var entries = Files.newDirectoryStream(path)) {
|
||||
for (var entry : entries) {
|
||||
deleteFolder(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Files.delete(path);
|
||||
}
|
||||
|
||||
// Dateien, die vom laufenden Server gesperrt sind oder im Backup keinen
|
||||
// Nutzen haben, werden beim Kopieren automatisch uebersprungen.
|
||||
private static final java.util.Set<String> SKIP_FILES = java.util.Set.of(
|
||||
"session.lock" // Vom Minecraft-Prozess exklusiv gesperrt
|
||||
);
|
||||
|
||||
private void copyFolder(Path source, Path target) throws IOException {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
Files.walk(source).forEach(src -> {
|
||||
String fileName = src.getFileName() != null ? src.getFileName().toString() : "";
|
||||
|
||||
if (SKIP_FILES.contains(fileName)) {
|
||||
plugin.getLogger().info("Ueberspringe gesperrte Datei beim Backup: " + fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Path dest = target.resolve(source.relativize(src));
|
||||
if (Files.isDirectory(src)) {
|
||||
if (!Files.exists(dest)) Files.createDirectories(dest);
|
||||
} else {
|
||||
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
errors.add(src + " → " + e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IOException("Fehler beim Kopieren folgender Dateien:\n" + String.join("\n", errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.viper.autoworldreset.scheduler;
|
||||
|
||||
import de.viper.autoworldreset.AutoWorldReset;
|
||||
import de.viper.autoworldreset.ResetManager;
|
||||
import de.viper.autoworldreset.util.CronParserUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class ResetScheduler {
|
||||
|
||||
private final AutoWorldReset plugin;
|
||||
private final ResetManager resetManager;
|
||||
private BukkitTask task;
|
||||
|
||||
// BUG FIX #4: Fehlende stopped-Flag hinzugefügt.
|
||||
// Ohne dieses Flag rief der Callback nach Ausführung scheduleNextReset()
|
||||
// erneut auf, selbst nachdem stop() bereits aufgerufen wurde. Das führte
|
||||
// dazu, dass der Scheduler nach einem stop() ungewollt weiter lief.
|
||||
private boolean stopped = false;
|
||||
|
||||
public ResetScheduler(AutoWorldReset plugin, ResetManager resetManager) {
|
||||
this.plugin = plugin;
|
||||
this.resetManager = resetManager;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
stopped = false;
|
||||
scheduleNextReset();
|
||||
}
|
||||
|
||||
private void scheduleNextReset() {
|
||||
if (stopped) return;
|
||||
|
||||
String cron = plugin.getConfig().getString("scheduler.cron");
|
||||
|
||||
// Null-Check (wird auch in CronParserUtil geprüft, aber defensive
|
||||
// Programmierung ist hier sinnvoll für eine klare Log-Meldung)
|
||||
if (cron == null || cron.isBlank()) {
|
||||
plugin.getLogger().warning("Kein Cron-Ausdruck unter 'scheduler.cron' konfiguriert!");
|
||||
return;
|
||||
}
|
||||
|
||||
Duration delay = CronParserUtil.parseCronToDelay(cron);
|
||||
|
||||
if (delay == null) {
|
||||
plugin.getLogger().warning("Ungültiger Cron-Ausdruck: '" + cron + "'. Scheduler wird nicht gestartet.");
|
||||
return;
|
||||
}
|
||||
|
||||
// BUG FIX #5: Delay von 0 oder negativ abfangen.
|
||||
// Wenn die nächste Ausführungszeit in der Vergangenheit liegt (z. B. wegen
|
||||
// Systemzeitproblemen), würde ein Delay von 0 Ticks zu einem Sofort-Reset führen.
|
||||
if (delay.isZero() || delay.isNegative()) {
|
||||
plugin.getLogger().warning("Berechneter Delay ist 0 oder negativ – Reset wird übersprungen, nächsten Termin berechnen.");
|
||||
// Warte 60 Sekunden und versuche es erneut
|
||||
task = Bukkit.getScheduler().runTaskLater(plugin, this::scheduleNextReset, 20L * 60);
|
||||
return;
|
||||
}
|
||||
|
||||
long ticks = delay.getSeconds() * 20L;
|
||||
plugin.getLogger().info("Nächster geplanter Reset in "
|
||||
+ delay.toHours() + "h " + (delay.toMinutes() % 60) + "min (" + ticks + " Ticks).");
|
||||
|
||||
task = Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (stopped) return; // BUG FIX #4 (Fortsetzung): Doppelte Prüfung im Callback
|
||||
plugin.getLogger().info("Geplanter Reset wird jetzt ausgeführt...");
|
||||
resetManager.resetWorld();
|
||||
scheduleNextReset();
|
||||
}, ticks);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
stopped = true;
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return !stopped && task != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.viper.autoworldreset.util;
|
||||
|
||||
import com.cronutils.model.Cron;
|
||||
import com.cronutils.model.time.ExecutionTime;
|
||||
import com.cronutils.parser.CronParser;
|
||||
import com.cronutils.model.definition.CronDefinitionBuilder;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
// BUG FIX #1: CronType von UNIX auf QUARTZ geändert.
|
||||
// UNIX-Cron hat nur 5 Felder (min h dom mon dow), aber config.yml verwendet
|
||||
// einen 6-feldrigen Quartz-Ausdruck ("0 30 18 * * *" = Sekunde/Minute/Stunde/...).
|
||||
// Zur Laufzeit warf das einen IllegalArgumentException beim Parsen.
|
||||
import static com.cronutils.model.CronType.QUARTZ;
|
||||
|
||||
public class CronParserUtil {
|
||||
|
||||
public static Duration parseCronToDelay(String cronExpression) {
|
||||
// BUG FIX #2: Null-/Leerstring-Check verhindert NullPointerException,
|
||||
// wenn der Schlüssel "scheduler.cron" in der config.yml fehlt.
|
||||
if (cronExpression == null || cronExpression.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(QUARTZ));
|
||||
Cron cron = parser.parse(cronExpression);
|
||||
cron.validate(); // BUG FIX #3: Explizite Validierung für frühzeitige,
|
||||
// verständliche Fehlermeldungen bei ungültigen Ausdrücken.
|
||||
ExecutionTime executionTime = ExecutionTime.forCron(cron);
|
||||
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
return executionTime.timeToNextExecution(now).orElse(null);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[AutoWorldReset] Ungültiger Cron-Ausdruck '"
|
||||
+ cronExpression + "': " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user