Update from Git Manager GUI

This commit is contained in:
2026-02-28 20:44:41 +01:00
parent 8f42a83f15
commit 8a702488be
355 changed files with 716 additions and 1502 deletions

View 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);
}
}

View 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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}