diff --git a/src/main/java/dev/viper/weathertime/WeatherTimeSyncPlugin.java b/src/main/java/dev/viper/weathertime/WeatherTimeSyncPlugin.java index 5c6bdb4..9ae64ca 100644 --- a/src/main/java/dev/viper/weathertime/WeatherTimeSyncPlugin.java +++ b/src/main/java/dev/viper/weathertime/WeatherTimeSyncPlugin.java @@ -5,15 +5,13 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.*; -import org.bukkit.block.Block; import org.bukkit.command.*; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import org.bukkit.entity.Snowman; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockFormEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.player.PlayerChangedWorldEvent; @@ -34,6 +32,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import dev.viper.weathertime.MetricsManager; import dev.viper.weathertime.UpdateChecker; @@ -47,16 +46,22 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { private String apiKey; private int updateInterval; private final Map worldConfigs = new HashMap<>(); - private final Map worldWeatherData = new HashMap<>(); + private final Map worldWeatherData = new ConcurrentHashMap<>(); + + // FIX: Scoreboard standardmäßig AUS – nur aktiv wenn der Spieler es in der GUI aktiviert hat private final Map> playersWithDisplay = new HashMap<>(); private final Map playerLocations = new HashMap<>(); private final Map playerGUIs = new HashMap<>(); private final Map playerScoreboards = new HashMap<>(); private final Map playerDisplayModes = new HashMap<>(); + + // FIX: Gecachte Wetterdaten pro Spieler (custom location) – vermeidet Blocking auf dem Main-Thread + private final Map playerWeatherCache = new ConcurrentHashMap<>(); + private BukkitAudiences audiences; private BukkitRunnable weatherUpdateTask; private BukkitRunnable syncTask; - private final Set processedLocations = new HashSet<>(); + private final Set processedLocations = ConcurrentHashMap.newKeySet(); private FileConfiguration langConfig; private MetricsManager metricsManager; @@ -74,22 +79,26 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { updateChecker = new UpdateChecker(this, 127846); updateChecker.getLatestVersion(version -> { + // FIX: NumberFormatException absichern falls Version kein valides Format hat String cleanVersion = version.replaceAll("[^0-9.]", "").trim(); String currentVersion = getDescription().getVersion(); - if (isVersionNewer(currentVersion, cleanVersion)) { - latestVersion = cleanVersion; + try { + if (isVersionNewer(currentVersion, cleanVersion)) { + latestVersion = cleanVersion; + getLogger().info("Neue Version verfügbar: " + cleanVersion + " (aktuell: " + currentVersion + ")"); + getLogger().info("Download: https://www.spigotmc.org/resources/127846/"); - getLogger().info("Neue Version verfügbar: " + cleanVersion + " (aktuell: " + currentVersion + ")"); - getLogger().info("Download: https://www.spigotmc.org/resources/127846/"); - - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.isOp()) { - player.sendMessage("§aEine neue Version von §eRealTimeWeather §aist verfügbar: §e" - + cleanVersion + " §7(aktuell: " + currentVersion + ")"); - player.sendMessage("§eDownload: §bhttps://www.spigotmc.org/resources/127846/"); + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.isOp()) { + player.sendMessage("§aEine neue Version von §eRealTimeWeather §aist verfügbar: §e" + + cleanVersion + " §7(aktuell: " + currentVersion + ")"); + player.sendMessage("§eDownload: §bhttps://www.spigotmc.org/resources/127846/"); + } } } + } catch (Exception e) { + getLogger().warning("Versionsvergleich fehlgeschlagen: " + e.getMessage()); } }); @@ -104,28 +113,39 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { startWeatherUpdateTask(); startSyncTask(); initializePlayerDisplays(); - + getLogger().info("=============================================="); getLogger().info("RealTimeWeather nutzt SCOREBOARD-Anzeige!"); - getLogger().info("Die Wetteranzeige erscheint rechts am Bildschirm."); + getLogger().info("Das Scoreboard ist standardmäßig AUSGEBLENDET."); + getLogger().info("Spieler können es per /wetter gui oder /toggleweather aktivieren."); getLogger().info("=============================================="); } + /** + * FIX: NumberFormatException durch try-catch abgesichert. + * Vergleicht semantische Versionen (z.B. "1.3.0" vs "1.4"). + */ private boolean isVersionNewer(String current, String latest) { + if (current == null || current.isEmpty() || latest == null || latest.isEmpty()) return false; String[] curParts = current.split("\\."); String[] latParts = latest.split("\\."); int length = Math.max(curParts.length, latParts.length); for (int i = 0; i < length; i++) { - int curNum = i < curParts.length ? Integer.parseInt(curParts[i]) : 0; - int latNum = i < latParts.length ? Integer.parseInt(latParts[i]) : 0; + int curNum, latNum; + try { + curNum = i < curParts.length ? Integer.parseInt(curParts[i].trim()) : 0; + } catch (NumberFormatException e) { + curNum = 0; + } + try { + latNum = i < latParts.length ? Integer.parseInt(latParts[i].trim()) : 0; + } catch (NumberFormatException e) { + latNum = 0; + } - if (latNum > curNum) { - return true; - } - if (latNum < curNum) { - return false; - } + if (latNum > curNum) return true; + if (latNum < curNum) return false; } return false; } @@ -168,10 +188,6 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { cfg.getString("defaults.location", "Berlin,de"), cfg.getString("defaults.units", "metric"), cfg.getString("defaults.time-format", "24h"), - cfg.getBoolean("defaults.display-actionbar", true), - cfg.getBoolean("defaults.display-weather-icon", true), - cfg.getString("defaults.display-position", "top-right"), - cfg.getInt("defaults.padding-right", 100), cfg.getBoolean("defaults.sync-in-game-weather", true) ); worldConfigs.put("defaults", defaultConfig); @@ -183,10 +199,6 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { cfg.getString("worlds." + worldName + ".location", defaultConfig.location), cfg.getString("worlds." + worldName + ".units", defaultConfig.units), cfg.getString("worlds." + worldName + ".time-format", defaultConfig.timeFormat), - cfg.getBoolean("worlds." + worldName + ".display-actionbar", defaultConfig.displayActionbar), - cfg.getBoolean("worlds." + worldName + ".display-weather-icon", defaultConfig.displayWeatherIcon), - cfg.getString("worlds." + worldName + ".display-position", defaultConfig.displayPosition), - cfg.getInt("worlds." + worldName + ".padding-right", defaultConfig.paddingRight), cfg.getBoolean("worlds." + worldName + ".sync-in-game-weather", defaultConfig.syncInGameWeather) ); worldConfigs.put(worldName, worldConfig); @@ -218,25 +230,33 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { for (World world : Bukkit.getWorlds()) { WorldConfig config = worldConfigs.getOrDefault(world.getName(), worldConfigs.get("defaults")); if (config.enabled) { + // Daylight-Cycle deaktivieren – Zeit wird vom Plugin gesetzt @SuppressWarnings("unchecked") GameRule doDaylightCycle = (GameRule) GameRule.getByName("doDaylightCycle"); if (doDaylightCycle != null) { world.setGameRule(doDaylightCycle, false); } + // Weather-Cycle deaktivieren – verhindert dass Minecraft das Wetter + // nach ein paar Minuten eigenständig zurücksetzt + @SuppressWarnings("unchecked") + GameRule doWeatherCycle = (GameRule) GameRule.getByName("doWeatherCycle"); + if (doWeatherCycle != null) { + world.setGameRule(doWeatherCycle, false); + } } } } + /** + * FIX: Scoreboard wird standardmäßig NICHT aktiviert. + * playersWithDisplay wird nur mit leeren Sets initialisiert. + * Kein createScoreboardForPlayer() beim Start. + */ private void initializePlayerDisplays() { playersWithDisplay.clear(); for (Player player : Bukkit.getOnlinePlayers()) { - Set worlds = new HashSet<>(); - for (World world : Bukkit.getWorlds()) { - WorldConfig config = worldConfigs.getOrDefault(world.getName(), worldConfigs.get("defaults")); - if (config.enabled && config.displayActionbar) worlds.add(world.getName()); - } - playersWithDisplay.put(player.getUniqueId(), worlds); - createScoreboardForPlayer(player); + // Leeres Set – Spieler hat noch kein Scoreboard aktiviert + playersWithDisplay.put(player.getUniqueId(), new HashSet<>()); } } @@ -272,7 +292,7 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { private void updateWeatherDataForAllWorlds() { for (World world : Bukkit.getWorlds()) { WorldConfig config = worldConfigs.getOrDefault(world.getName(), worldConfigs.get("defaults")); - + if (!config.enabled) { worldWeatherData.remove(world.getName()); continue; @@ -301,26 +321,7 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { public void run() { syncMinecraftTime(world, weatherData.getDateTime()); if (config.syncInGameWeather) { - switch (weatherData.getWeatherMain().toLowerCase()) { - case "rain": - case "drizzle": - world.setStorm(true); - world.setThundering(false); - break; - case "thunderstorm": - world.setStorm(true); - world.setThundering(true); - break; - case "snow": - world.setStorm(true); - world.setThundering(false); - spawnTemporarySnowInWorld(world, WeatherTimeSyncPlugin.this); - spawnTemporarySnowmen(world, 25, WeatherTimeSyncPlugin.this); - break; - default: - world.setStorm(false); - world.setThundering(false); - } + syncInGameWeather(world, weatherData); } } }.runTask(this); @@ -329,78 +330,153 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { getLogger().warning("Fehler beim Abrufen von Wetter/Zeit für Welt " + world.getName() + ": " + e.getMessage()); } } + + // Custom-Location-Wetterdaten für Spieler asynchron aktualisieren (kein Blocking auf dem Main-Thread) + refreshPlayerWeatherCaches(); } - private void spawnTemporarySnowInWorld(World world, JavaPlugin plugin) { - List snowBlocks = new ArrayList<>(); + /** + * Synchronisiert das Minecraft-Ingame-Wetter mit den echten API-Daten. + * + * Mapping der OpenWeatherMap-Wettertypen auf Minecraft: + * + * Thunderstorm → Gewitter (Sturm + Donner) + * Squall → Gewitter (böiger Wind/Sturm) + * Tornado → Gewitter (stärkste Stufe) + * Rain → Regen (Sturm, kein Donner) + * Drizzle → Nieselregen (Sturm, kein Donner) + * Snow → Schnee (Sturm; in kalten Biomen schneit es automatisch) + * Mist → Nebel/Dunst → klares Wetter (Minecraft hat keinen Nebel-Zustand) + * Smoke → Rauch → klares Wetter + * Haze → Dunst → klares Wetter + * Dust → Staub → klares Wetter + * Sand → Sandsturm → klares Wetter + * Ash → Asche → klares Wetter + * Fog → Nebel → klares Wetter + * Clouds → Bewölkt → klares Wetter (Minecraft hat keine reine Bewölkungsstufe) + * Clear → Sonnig → klares Wetter + * + * Wichtig: doWeatherCycle muss false sein (wird in setDoDaylightCycleForWorlds gesetzt), + * damit Minecraft das Wetter nicht nach wenigen Minuten eigenständig zurücksetzt. + */ + private void syncInGameWeather(World world, WeatherTimeData data) { + // Wetter-Duration etwas länger als das Update-Interval setzen, + // damit kein Flickern zwischen den API-Abfragen entsteht. + // Auch bei deaktiviertem doWeatherCycle empfohlen für maximale Stabilität. + int durationTicks = (updateInterval + 120) * 20; - for (org.bukkit.Chunk chunk : world.getLoadedChunks()) { - int chunkX = chunk.getX() << 4; - int chunkZ = chunk.getZ() << 4; + switch (data.getWeatherMain().toLowerCase()) { - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - if (Math.random() > 0.7) continue; - int worldX = chunkX + x; - int worldZ = chunkZ + z; - int worldY = world.getHighestBlockYAt(worldX, worldZ); + // --- Gewitter-Gruppe --- + case "thunderstorm": + // Klassisches Gewitter mit Blitz und Donner + world.setStorm(true); + world.setThundering(true); + world.setWeatherDuration(durationTicks); + world.setThunderDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Gewitter (Thunderstorm)"); + break; - Block ground = world.getBlockAt(worldX, worldY - 1, worldZ); - Block above = world.getBlockAt(worldX, worldY, worldZ); + case "squall": + // Bö / Sturmböe – starker Wind mit Gewittercharakter + world.setStorm(true); + world.setThundering(true); + world.setWeatherDuration(durationTicks); + world.setThunderDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Sturmböe (Squall)"); + break; - if (ground.getType().isSolid() && above.getType() == Material.AIR) { - above.setType(Material.SNOW); - snowBlocks.add(above); - } - } - } + case "tornado": + // Tornado – stärkste Wetterstufe, Gewitter als Annäherung + world.setStorm(true); + world.setThundering(true); + world.setWeatherDuration(durationTicks); + world.setThunderDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Tornado"); + break; + + // --- Regen-Gruppe --- + case "rain": + // Normaler Regen + world.setStorm(true); + world.setThundering(false); + world.setWeatherDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Regen (Rain)"); + break; + + case "drizzle": + // Nieselregen – in Minecraft gleich wie Regen + world.setStorm(true); + world.setThundering(false); + world.setWeatherDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Nieselregen (Drizzle)"); + break; + + // --- Schnee --- + case "snow": + // Schneefall – Minecraft lässt es in kalten Biomen bei storm=true automatisch schneien. + // Kein Snowmen/Snow-Block-Hack nötig – das Spiel kümmert sich selbst darum. + world.setStorm(true); + world.setThundering(false); + world.setWeatherDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Schnee (Snow)"); + break; + + // --- Atmosphärische Zustände (Nebel, Dunst, Staub, etc.) --- + // Minecraft hat keinen echten Nebel-Wetterzustand. + // Klares Wetter ist die sinnvollste Annäherung. + case "mist": + case "smoke": + case "haze": + case "dust": + case "sand": + case "ash": + case "fog": + world.setStorm(false); + world.setThundering(false); + world.setWeatherDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Atmosphärisch (" + data.getWeatherMain() + ") → klar"); + break; + + // --- Bewölkt & Klar --- + case "clouds": + // Minecraft unterscheidet nicht zwischen klar und bewölkt → kein Sturm + world.setStorm(false); + world.setThundering(false); + world.setWeatherDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Bewölkt (Clouds) → klar"); + break; + + case "clear": + default: + // Klarer Himmel oder unbekannter Typ → sicherer Fallback auf klar + world.setStorm(false); + world.setThundering(false); + world.setWeatherDuration(durationTicks); + getLogger().fine("Wetter [" + world.getName() + "]: Klar (Clear / unbekannt: " + data.getWeatherMain() + ")"); + break; } - - new BukkitRunnable() { - @Override - public void run() { - for (Block b : snowBlocks) { - if (b.getType() == Material.SNOW) { - b.setType(Material.AIR); - } - } - } - }.runTaskLater(plugin, 180 * 20L); } - private void spawnTemporarySnowmen(World world, int count, JavaPlugin plugin) { - List snowmen = new ArrayList<>(); - Random random = new Random(); + /** + * Spieler mit eigener Location bekommen ihre Wetterdaten asynchron gecacht. + * updateScoreboardForWorld() nutzt dann nur noch den Cache – kein API-Call auf dem Main-Thread. + */ + private void refreshPlayerWeatherCaches() { + for (Map.Entry entry : playerLocations.entrySet()) { + UUID playerId = entry.getKey(); + String location = entry.getValue(); + if (location == null || location.isEmpty()) continue; - List loadedChunks = Arrays.asList(world.getLoadedChunks()); - - for (int i = 0; i < count; i++) { - if (loadedChunks.isEmpty()) break; - org.bukkit.Chunk chunk = loadedChunks.get(random.nextInt(loadedChunks.size())); - - int chunkX = chunk.getX() << 4; - int chunkZ = chunk.getZ() << 4; - int x = chunkX + random.nextInt(16); - int z = chunkZ + random.nextInt(16); - int y = world.getHighestBlockYAt(x, z); - - if (world.getBlockAt(x, y - 1, z).getType().isSolid()) { - Snowman snowman = (Snowman) world.spawnEntity( - new Location(world, x + 0.5, y, z + 0.5), - EntityType.SNOW_GOLEM - ); - snowmen.add(snowman); + // Läuft bereits asynchron (aufgerufen aus weatherUpdateTask) + try { + WeatherFetcher fetcher = new WeatherFetcher(apiKey, location, "metric", getLogger()); + WeatherTimeData data = fetcher.fetchAsWeatherTimeData(); + playerWeatherCache.put(playerId, data); + } catch (Exception e) { + getLogger().warning("Fehler beim Aktualisieren des Wetter-Caches für Spieler " + playerId + ": " + e.getMessage()); } } - - new BukkitRunnable() { - @Override - public void run() { - for (Snowman s : snowmen) { - if (!s.isDead()) s.remove(); - } - } - }.runTaskLater(plugin, 180 * 20L); } private void syncMinecraftTime(World world, ZonedDateTime dateTime) { @@ -416,32 +492,44 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { } } + /** + * Erstellt ein neues Scoreboard für den Spieler und weist es zu. + * Wird nur aufgerufen wenn der Spieler das Scoreboard explizit aktiviert. + */ private void createScoreboardForPlayer(Player player) { Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); - Objective objective = scoreboard.registerNewObjective("weather", "dummy", + Objective objective = scoreboard.registerNewObjective("weather", "dummy", ChatColor.GOLD + "" + ChatColor.BOLD + "⛅ Wetter Info"); objective.setDisplaySlot(DisplaySlot.SIDEBAR); - + player.setScoreboard(scoreboard); playerScoreboards.put(player.getUniqueId(), scoreboard); } + /** + * Entfernt das Scoreboard vom Spieler (setzt auf Main-Scoreboard zurück). + */ + private void removeScoreboardFromPlayer(Player player) { + player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); + playerScoreboards.remove(player.getUniqueId()); + } + + /** + * FIX: API-Calls nur noch aus dem Cache (playerWeatherCache) lesen. + * Kein blockierender fetchAsWeatherTimeData()-Aufruf auf dem Main-Thread mehr. + */ private void updateScoreboardForWorld(World world, WeatherTimeData data) { WorldConfig config = worldConfigs.getOrDefault(world.getName(), worldConfigs.get("defaults")); - - if (!config.enabled || !config.displayActionbar) { - return; - } + + if (!config.enabled) return; for (Player player : world.getPlayers()) { Set displayWorlds = playersWithDisplay.getOrDefault(player.getUniqueId(), new HashSet<>()); - + if (!displayWorlds.contains(world.getName())) { - // Scoreboard entfernen wenn disabled - Scoreboard scoreboard = playerScoreboards.get(player.getUniqueId()); - if (scoreboard != null) { - player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); - playerScoreboards.remove(player.getUniqueId()); + // Scoreboard entfernen wenn für diese Welt deaktiviert + if (playerScoreboards.containsKey(player.getUniqueId())) { + removeScoreboardFromPlayer(player); } continue; } @@ -456,19 +544,19 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { if (objective == null) continue; // Alte Scores löschen - for (String entry : scoreboard.getEntries()) { + for (String entry : new HashSet<>(scoreboard.getEntries())) { scoreboard.resetScores(entry); } + // FIX: Wetterdaten aus Cache holen statt API-Call auf Main-Thread WeatherTimeData playerData = data; String location = playerLocations.getOrDefault(player.getUniqueId(), ""); if (!location.isEmpty()) { - try { - WeatherFetcher fetcher = new WeatherFetcher(apiKey, location, config.units, getLogger()); - playerData = fetcher.fetchAsWeatherTimeData(); - } catch (Exception e) { - continue; + WeatherTimeData cached = playerWeatherCache.get(player.getUniqueId()); + if (cached != null) { + playerData = cached; } + // Kein else – bei fehlendem Cache einfach Welt-Daten verwenden } // Display Mode des Spielers @@ -491,8 +579,8 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { ChatColor windColor = WeatherColors.getWindSpeedColor(playerData.getWindSpeed()); // Trennlinie - String separator = ChatColor.DARK_GRAY + "~" + ChatColor.GRAY + "~" + ChatColor.DARK_GRAY + "~" + - ChatColor.GRAY + "~" + ChatColor.DARK_GRAY + "~" + ChatColor.GRAY + "~" + + String separator = ChatColor.DARK_GRAY + "~" + ChatColor.GRAY + "~" + ChatColor.DARK_GRAY + "~" + + ChatColor.GRAY + "~" + ChatColor.DARK_GRAY + "~" + ChatColor.GRAY + "~" + ChatColor.DARK_GRAY + "~" + ChatColor.GRAY + "~" + ChatColor.DARK_GRAY + "~"; int line = 20; @@ -517,20 +605,17 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { objective.getScore(weatherColor + weatherIcon + " " + ChatColor.GRAY + weatherMain).setScore(line--); objective.getScore(tempColor + "🌡 " + String.format("%.1f%s", temp, tempUnit)).setScore(line--); objective.getScore(humidityColor + "💧 " + playerData.getHumidity() + "%").setScore(line--); - - // Windrichtung mit Pfeil + String windDir = WindDirection.getDirection(playerData.getWindDeg()); String windArrow = WindDirection.getArrow(playerData.getWindDeg()); - objective.getScore(windColor + "🌬 " + String.format("%.1f", playerData.getWindSpeed()) + + objective.getScore(windColor + "🌬 " + String.format("%.1f", playerData.getWindSpeed()) + (config.units.equalsIgnoreCase("metric") ? " m/s " : " mph ") + windArrow + " " + windDir).setScore(line--); - + objective.getScore(separator + "2").setScore(line--); objective.getScore(ChatColor.GOLD + "🌅 " + playerData.getSunrise().format(DateTimeFormatter.ofPattern("HH:mm"))).setScore(line--); objective.getScore(ChatColor.DARK_RED + "🌇 " + playerData.getSunset().format(DateTimeFormatter.ofPattern("HH:mm"))).setScore(line--); - - // Mondphase objective.getScore(ChatColor.LIGHT_PURPLE + MoonPhase.getEmoji(world) + " " + MoonPhase.getName(world)).setScore(line--); - + objective.getScore(separator + "3").setScore(line--); objective.getScore(ChatColor.DARK_AQUA + "📍 " + cityName).setScore(line--); objective.getScore(" ").setScore(line--); @@ -543,42 +628,33 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { objective.getScore(separator + "1").setScore(line--); objective.getScore(weatherColor + weatherIcon + " " + ChatColor.GRAY + weatherMain).setScore(line--); objective.getScore(tempColor + "🌡 " + String.format("%.1f%s", temp, tempUnit)).setScore(line--); - - // Gefühlte Temperatur + double feelsLike = playerData.getFeelsLike(config.units.equalsIgnoreCase("metric") ? "C" : "F"); ChatColor feelsLikeColor = WeatherColors.getTemperatureColor(playerData.getFeelsLike("C")); objective.getScore(feelsLikeColor + "🌡️ Gefühlt: " + String.format("%.1f%s", feelsLike, tempUnit)).setScore(line--); - + objective.getScore(humidityColor + "💧 " + playerData.getHumidity() + "%").setScore(line--); - - // Windrichtung mit Pfeil + String windDir = WindDirection.getDirection(playerData.getWindDeg()); String windArrow = WindDirection.getArrow(playerData.getWindDeg()); - objective.getScore(windColor + "🌬 " + String.format("%.1f", playerData.getWindSpeed()) + + objective.getScore(windColor + "🌬 " + String.format("%.1f", playerData.getWindSpeed()) + (config.units.equalsIgnoreCase("metric") ? " m/s " : " mph ") + windArrow + " " + windDir).setScore(line--); - - // Luftdruck + objective.getScore(ChatColor.GRAY + "📊 " + playerData.getPressure() + " hPa").setScore(line--); - - // Wolkendichte objective.getScore(ChatColor.WHITE + "☁️ Wolken: " + playerData.getClouds() + "%").setScore(line--); - - // Sichtweite + int visKm = playerData.getVisibility() / 1000; objective.getScore(ChatColor.BLUE + "👁️ Sicht: " + visKm + " km").setScore(line--); - - // UV-Index + int uvIndex = playerData.getUVIndex(); ChatColor uvColor = WeatherColors.getUVIndexColor(uvIndex); objective.getScore(uvColor + "☀️ UV: " + uvIndex).setScore(line--); - + objective.getScore(separator + "2").setScore(line--); objective.getScore(ChatColor.GOLD + "🌅 " + playerData.getSunrise().format(DateTimeFormatter.ofPattern("HH:mm"))).setScore(line--); objective.getScore(ChatColor.DARK_RED + "🌇 " + playerData.getSunset().format(DateTimeFormatter.ofPattern("HH:mm"))).setScore(line--); - - // Mondphase objective.getScore(ChatColor.LIGHT_PURPLE + MoonPhase.getPhase(world)).setScore(line--); - + objective.getScore(separator + "3").setScore(line--); objective.getScore(ChatColor.DARK_AQUA + "📍 " + cityName).setScore(line--); objective.getScore(" ").setScore(line--); @@ -598,30 +674,81 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { String localizedWeather = getLocalizedWeatherMain(data.getWeatherMain(), countryCode); String tempUnit = config.units.equalsIgnoreCase("metric") ? "°C" : "°F"; String city = config.location.split(",")[0]; - String message = String.format("%s: %s, %.1f%s", city, localizedWeather, data.getTemp(config.units.equalsIgnoreCase("metric") ? "C" : "F"), tempUnit); + String message = String.format("%s: %s, %.1f%s", city, localizedWeather, + data.getTemp(config.units.equalsIgnoreCase("metric") ? "C" : "F"), tempUnit); player.sendMessage(ChatColor.GREEN + message); + + // Scoreboard-Status prüfen: wenn in neuer Welt nicht aktiviert → entfernen + Set displayWorlds = playersWithDisplay.getOrDefault(player.getUniqueId(), new HashSet<>()); + if (!displayWorlds.contains(world.getName()) && playerScoreboards.containsKey(player.getUniqueId())) { + removeScoreboardFromPlayer(player); + } } + /** + * FIX: Beim Join wird das Scoreboard NICHT automatisch aktiviert. + * Spieler starten mit leerem Display-Set. + */ @EventHandler public void onPlayerJoinForDisplay(PlayerJoinEvent event) { Player player = event.getPlayer(); - Set worlds = new HashSet<>(); - for (World world : Bukkit.getWorlds()) { - WorldConfig config = worldConfigs.getOrDefault(world.getName(), worldConfigs.get("defaults")); - if (config.enabled && config.displayActionbar) { - worlds.add(world.getName()); - } + // Nur leeres Set initialisieren – kein automatisches Scoreboard + playersWithDisplay.put(player.getUniqueId(), new HashSet<>()); + + // Gespeicherte Location laden (falls vorhanden) + PlayerConfig playerConfig = new PlayerConfig(player.getUniqueId()); + String savedLocation = playerConfig.getLocation(); + if (savedLocation != null && !savedLocation.isEmpty()) { + playerLocations.put(player.getUniqueId(), savedLocation); } - playersWithDisplay.put(player.getUniqueId(), worlds); - createScoreboardForPlayer(player); } + /** + * Verhindert dass Schnee ingame liegenbleibt wenn das Plugin Schneewetter setzt. + * + * Minecraft akkumuliert Schneelagen (SNOW) und Eis (ICE / FROSTED_ICE) über Zeit + * in kalten Biomen sobald storm=true gesetzt ist. Da wir das Wetter dauerhaft + * steuern, würde Schnee sonst endlos wachsen. + * + * Lösung: BlockFormEvent abfangen und canceln wenn: + * - Die Welt vom Plugin verwaltet wird (enabled + syncInGameWeather) + * - Das aktuelle API-Wetter "Snow" ist + * - Der entstehende Block eine Schneelage oder Eis ist + */ + @EventHandler(ignoreCancelled = true) + public void onBlockForm(BlockFormEvent event) { + World world = event.getBlock().getWorld(); + WorldConfig config = worldConfigs.getOrDefault(world.getName(), worldConfigs.get("defaults")); + + // Nur in vom Plugin verwalteten Welten eingreifen + if (!config.enabled || !config.syncInGameWeather) return; + + WeatherTimeData data = worldWeatherData.get(world.getName()); + if (data == null) return; + + // Nur bei Schnee-Wetter aktiv + if (!data.getWeatherMain().equalsIgnoreCase("snow")) return; + + Material formed = event.getNewState().getType(); + if (formed == Material.SNOW || formed == Material.ICE || formed == Material.FROSTED_ICE) { + event.setCancelled(true); + } + } + + /** + * FIX: instanceof-Check vor Cast auf Player hinzugefügt. + * FIX: GUI-Toggle korrekt mit createScoreboardForPlayer verknüpft. + */ @EventHandler public void onInventoryClick(InventoryClickEvent event) { + // FIX: Sicherheitscheck – nur Player können die GUI bedienen + if (!(event.getWhoClicked() instanceof Player)) return; Player player = (Player) event.getWhoClicked(); + Inventory expectedGUI = playerGUIs.get(player.getUniqueId()); if (expectedGUI == null || event.getInventory() != expectedGUI) return; event.setCancelled(true); + ItemStack clickedItem = event.getCurrentItem(); if (clickedItem == null || clickedItem.getType() == Material.AIR) return; @@ -632,24 +759,35 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { player.closeInventory(); audiences.player(player).sendMessage(Component.text( getLocalizedMessage("gui_setlocation_prompt", countryCode), NamedTextColor.YELLOW)); + } else if (clickedItem.getType() == Material.REDSTONE_TORCH) { + // FIX: Scoreboard korrekt aktivieren/deaktivieren String worldName = player.getWorld().getName(); Set worlds = playersWithDisplay.computeIfAbsent(player.getUniqueId(), k -> new HashSet<>()); if (worlds.contains(worldName)) { worlds.remove(worldName); + removeScoreboardFromPlayer(player); audiences.player(player).sendMessage(Component.text( - getLocalizedMessage("toggle_disabled", countryCode, "world", worldName), NamedTextColor.RED)); + ChatColor.RED + "⛅ Scoreboard-Wetteranzeige deaktiviert.", NamedTextColor.RED)); } else { worlds.add(worldName); + createScoreboardForPlayer(player); audiences.player(player).sendMessage(Component.text( - getLocalizedMessage("toggle_enabled", countryCode, "world", worldName), NamedTextColor.GREEN)); + ChatColor.GREEN + "⛅ Scoreboard-Wetteranzeige aktiviert!", NamedTextColor.GREEN)); } + // GUI offen lassen damit Spieler Status sehen kann + player.closeInventory(); + player.openInventory(createWeatherGUI(player)); + } else if (clickedItem.getType() == Material.PAPER) { player.closeInventory(); new WeatherForecastCommand().onCommand(player, null, "weatherforecast", new String[]{}); + } else if (clickedItem.getType() == Material.OAK_SIGN) { player.closeInventory(); - String infoMessage = "RealTimeWeather Plugin\nVersion: 1.3\nAutor: M_Viper\nGetestete Minecraft-Version: 1.21.1 - 1.21.11"; + String infoMessage = getDescription().getName() + " Plugin" + + "\nVersion: " + getDescription().getVersion() + + "\nAutor: " + String.join(", ", getDescription().getAuthors()); audiences.player(player).sendMessage(Component.text(infoMessage, NamedTextColor.AQUA)); } } @@ -699,36 +837,55 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { return countryCode; } + /** + * FIX: GUI-Toggle-Button zeigt korrekt "Scoreboard" an (statt "Bossbar"). + * Toggle-Button aktualisiert seinen Status live (✅ aktiv / ❌ inaktiv). + */ private Inventory createWeatherGUI(Player player) { String countryCode = getCountryCode(worldConfigs.getOrDefault(player.getWorld().getName(), worldConfigs.get("defaults")).location); Inventory inv = Bukkit.createInventory(null, 9, getLocalizedMessage("gui_title", countryCode)); - ItemStack setLocation = new ItemStack(Material.BOOK); - ItemStack toggleWeather = new ItemStack(Material.REDSTONE_TORCH); - ItemStack forecast = new ItemStack(Material.PAPER); - ItemStack info = new ItemStack(Material.OAK_SIGN); + // --- Standort setzen --- + ItemStack setLocation = new ItemStack(Material.BOOK); ItemMeta setLocationMeta = setLocation.getItemMeta(); - setLocationMeta.setDisplayName(getLocalizedMessage("gui_setlocation", countryCode)); - setLocationMeta.setLore(Collections.singletonList(getLocalizedMessage("gui_setlocation_lore", countryCode))); + setLocationMeta.setDisplayName(ChatColor.YELLOW + "📍 Standort setzen"); + setLocationMeta.setLore(Collections.singletonList(ChatColor.GRAY + "Klicke um deinen Standort zu ändern")); setLocation.setItemMeta(setLocationMeta); - ItemMeta toggleWeatherMeta = toggleWeather.getItemMeta(); - toggleWeatherMeta.setDisplayName(getLocalizedMessage("gui_toggleweather", countryCode)); - toggleWeatherMeta.setLore(Collections.singletonList(getLocalizedMessage("gui_toggleweather_lore", countryCode))); - toggleWeather.setItemMeta(toggleWeatherMeta); + // --- Scoreboard umschalten --- + // FIX: Korrekte Beschriftung als "Scoreboard" (nicht mehr "Bossbar") + boolean scoreboardActive = playersWithDisplay + .getOrDefault(player.getUniqueId(), new HashSet<>()) + .contains(player.getWorld().getName()); + ItemStack toggleScoreboard = new ItemStack(Material.REDSTONE_TORCH); + ItemMeta toggleMeta = toggleScoreboard.getItemMeta(); + toggleMeta.setDisplayName(ChatColor.GOLD + "⛅ Scoreboard-Wetteranzeige"); + List toggleLore = new ArrayList<>(); + toggleLore.add(ChatColor.GRAY + "Zeigt Wetterdaten im Scoreboard (rechts)"); + toggleLore.add(scoreboardActive + ? ChatColor.GREEN + "✅ Aktuell: Aktiv" + : ChatColor.RED + "❌ Aktuell: Inaktiv"); + toggleLore.add(ChatColor.GRAY + "Klicke zum Umschalten"); + toggleMeta.setLore(toggleLore); + toggleScoreboard.setItemMeta(toggleMeta); + + // --- Vorhersage --- + ItemStack forecast = new ItemStack(Material.PAPER); ItemMeta forecastMeta = forecast.getItemMeta(); - forecastMeta.setDisplayName(getLocalizedMessage("gui_forecast", countryCode)); - forecastMeta.setLore(Collections.singletonList(getLocalizedMessage("gui_forecast_lore", countryCode))); + forecastMeta.setDisplayName(ChatColor.AQUA + "📋 Wettervorhersage"); + forecastMeta.setLore(Collections.singletonList(ChatColor.GRAY + "5-Tage-Vorhersage anzeigen")); forecast.setItemMeta(forecastMeta); + // --- Info --- + ItemStack info = new ItemStack(Material.OAK_SIGN); ItemMeta infoMeta = info.getItemMeta(); - infoMeta.setDisplayName(getLocalizedMessage("gui_info", countryCode)); - infoMeta.setLore(Collections.singletonList(getLocalizedMessage("gui_info_lore", countryCode))); + infoMeta.setDisplayName(ChatColor.WHITE + "ℹ Plugin-Info"); + infoMeta.setLore(Collections.singletonList(ChatColor.GRAY + "Plugin-Informationen anzeigen")); info.setItemMeta(infoMeta); inv.setItem(2, setLocation); - inv.setItem(3, toggleWeather); + inv.setItem(3, toggleScoreboard); inv.setItem(4, forecast); inv.setItem(5, info); @@ -739,15 +896,18 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { private class PlayerConfig { private final File configFile; private final FileConfiguration config; + public PlayerConfig(UUID playerId) { File playerDir = new File(getDataFolder(), "players"); playerDir.mkdirs(); configFile = new File(playerDir, playerId.toString() + ".yml"); config = YamlConfiguration.loadConfiguration(configFile); } + public String getLocation() { return config.getString("location", ""); } + public void setLocation(String location) { config.set("location", location); try { @@ -815,10 +975,8 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { processedLocations.clear(); initializePlayerDisplays(); setDoDaylightCycleForWorlds(); - startWeatherUpdateTask(); + startWeatherUpdateTask(); // startet async und ruft updateWeatherDataForAllWorlds() intern auf startSyncTask(); - processedLocations.clear(); - updateWeatherDataForAllWorlds(); audiences.sender(sender).sendMessage(Component.text(getLocalizedMessage("reload_success", countryCode), NamedTextColor.GREEN)); return true; } @@ -838,10 +996,12 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { public void run() { try { WeatherFetcher fetcher = new WeatherFetcher(apiKey, location, "metric", getLogger()); - fetcher.fetchAsWeatherTimeData(); + WeatherTimeData newData = fetcher.fetchAsWeatherTimeData(); PlayerConfig playerConfig = new PlayerConfig(player.getUniqueId()); playerConfig.setLocation(location); playerLocations.put(player.getUniqueId(), location); + // FIX: Sofort in Cache speichern + playerWeatherCache.put(player.getUniqueId(), newData); String cc = getCountryCode(location); audiences.player(player).sendMessage(Component.text(getLocalizedMessage("location_set", cc, "location", location), NamedTextColor.GREEN)); } catch (Exception e) { @@ -873,7 +1033,8 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { String localizedWeather = getLocalizedWeatherMain(data.getWeatherMain(), cc); String tempUnit = config.units.equalsIgnoreCase("metric") ? "°C" : "°F"; String city = queryLocation.split(",")[0]; - String message = String.format("%s: %s, %.1f%s", city, localizedWeather, data.getTemp(config.units.equalsIgnoreCase("metric") ? "C" : "F"), tempUnit); + String message = String.format("%s: %s, %.1f%s", city, localizedWeather, + data.getTemp(config.units.equalsIgnoreCase("metric") ? "C" : "F"), tempUnit); audiences.player(queryPlayer).sendMessage(Component.text(message, NamedTextColor.GREEN)); } catch (Exception e) { audiences.player(queryPlayer).sendMessage(Component.text(getLocalizedMessage("forecast_error", countryCode, "location", queryLocation, "error", e.getMessage()), NamedTextColor.RED)); @@ -883,7 +1044,11 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { return true; } case "info": { - String infoMessage = "RealTimeWeather Plugin\nVersion: 1.3\nAutor: M_Viper\nGetestete Minecraft-Version: 1.21.1 - 1.21.11\n\nDie Wetteranzeige erscheint als SCOREBOARD rechts am Bildschirm."; + String infoMessage = getDescription().getName() + " Plugin" + + "\nVersion: " + getDescription().getVersion() + + "\nAutor: " + String.join(", ", getDescription().getAuthors()) + + "\n\nDas Scoreboard ist standardmäßig ausgeblendet." + + "\nAktiviere es per /wetter gui oder /toggleweather."; audiences.sender(sender).sendMessage(Component.text(infoMessage, NamedTextColor.AQUA)); return true; } @@ -908,7 +1073,7 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { return true; } Player modePlayer = (Player) sender; - + if (args.length != 2) { DisplayMode currentMode = playerDisplayModes.getOrDefault(modePlayer.getUniqueId(), DisplayMode.STANDARD); modePlayer.sendMessage(ChatColor.YELLOW + "Aktueller Anzeigemodus: " + ChatColor.GREEN + currentMode.getDisplayName()); @@ -918,13 +1083,12 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { modePlayer.sendMessage(ChatColor.GRAY + "- " + ChatColor.AQUA + "detailed" + ChatColor.GRAY + ": Alle verfügbaren Daten"); return true; } - + DisplayMode newMode = DisplayMode.fromString(args[1]); playerDisplayModes.put(modePlayer.getUniqueId(), newMode); - + modePlayer.sendMessage(ChatColor.GREEN + "Anzeigemodus geändert zu: " + ChatColor.GOLD + newMode.getDisplayName()); modePlayer.sendMessage(ChatColor.GRAY + "Das Scoreboard wird beim nächsten Update aktualisiert."); - return true; } default: @@ -971,13 +1135,13 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { String weatherMain = forecast.getJSONArray("weather").getJSONObject(0).getString("main"); double temp = forecast.getJSONObject("main").getDouble("temp"); ZonedDateTime dateTime = Instant.ofEpochSecond(dt).atZone(ZoneId.of("UTC")); - String localizedWeatherMain = getLocalizedWeatherMain(weatherMain, countryCode); message = message.append(Component.newline()) .append(Component.text( dateTime.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) + ": " + - localizedWeatherMain + ", " + String.format("%.1f%s", temp, config.units.equalsIgnoreCase("metric") ? "°C" : "°F"), + localizedWeatherMain + ", " + String.format("%.1f%s", temp, + config.units.equalsIgnoreCase("metric") ? "°C" : "°F"), NamedTextColor.GREEN)); } audiences.player(player).sendMessage(message); @@ -1013,14 +1177,11 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { Set worlds = playersWithDisplay.computeIfAbsent(player.getUniqueId(), k -> new HashSet<>()); if (worlds.contains(worldName)) { worlds.remove(worldName); - // Scoreboard entfernen - player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard()); - playerScoreboards.remove(player.getUniqueId()); + removeScoreboardFromPlayer(player); audiences.player(player).sendMessage(Component.text( getLocalizedMessage("toggle_disabled", countryCode, "world", worldName), NamedTextColor.RED)); } else { worlds.add(worldName); - // Scoreboard erstellen createScoreboardForPlayer(player); audiences.player(player).sendMessage(Component.text( getLocalizedMessage("toggle_enabled", countryCode, "world", worldName), NamedTextColor.GREEN)); @@ -1034,24 +1195,15 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener { String location; String units; String timeFormat; - boolean displayActionbar; - boolean displayWeatherIcon; - String displayPosition; - int paddingRight; boolean syncInGameWeather; public WorldConfig(boolean enabled, String location, String units, String timeFormat, - boolean displayActionbar, boolean displayWeatherIcon, - String displayPosition, int paddingRight, boolean syncInGameWeather) { + boolean syncInGameWeather) { this.enabled = enabled; this.location = location; this.units = units; this.timeFormat = timeFormat; - this.displayActionbar = displayActionbar; - this.displayWeatherIcon = displayWeatherIcon; - this.displayPosition = displayPosition; - this.paddingRight = paddingRight; this.syncInGameWeather = syncInGameWeather; } } -} +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index d5eb22d..606a941 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,39 +1,61 @@ -# OpenWeatherMap API-Key – hier eintragen +# ============================================================ +# RealTimeWeather – config.yml +# ============================================================ + +# OpenWeatherMap API-Key +# Kostenlos erhältlich unter: https://openweathermap.org/api api-key: "DEIN_API_KEY_HIER" -# Update-Intervall in Sekunden (Standard: 60) +# Wie oft Wetter & Zeit von der API abgerufen werden (in Sekunden) +# Empfohlen: 60–300 (zu niedrige Werte können das API-Limit überschreiten) update-interval: 60 +# ------------------------------------------------------------ +# Standard-Einstellungen (gelten wenn keine Welt-spezifischen +# Einstellungen definiert sind) +# ------------------------------------------------------------ defaults: + # Plugin für diese Welt aktiv? enabled: true - location: "Berlin,de" # Stadt,Land - units: "metric" # metric = °C, imperial = °F - time-format: "24h" # oder "12h" - display-actionbar: true - display-weather-icon: true - display-position: "top-right" - padding-right: 100 + + # Standort für Wetter & Zeit – Format: "Stadt,Länderkürzel" + # Beispiele: "Berlin,de" | "London,gb" | "New York,us" + location: "Berlin,de" + + # Temperatureinheit: "metric" (°C) oder "imperial" (°F) + units: "metric" + + # Uhrzeitformat: "24h" oder "12h" + time-format: "24h" + + # Soll das Ingame-Wetter mit der echten API synchronisiert werden? + # (Regen, Gewitter, Schnee, Klar etc.) sync-in-game-weather: true +# ------------------------------------------------------------ +# Welt-spezifische Einstellungen +# Nicht definierte Felder erben vom "defaults"-Block oben. +# ------------------------------------------------------------ worlds: world: enabled: true location: "Berlin,de" units: "metric" time-format: "24h" - display-actionbar: true - display-weather-icon: true - display-position: "top-left" - padding-right: 50 sync-in-game-weather: true - + world_nether: + # Nether hat kein Wetter/Tageslicht – Plugin hier deaktivieren + enabled: false + location: "Berlin,de" + units: "metric" + time-format: "24h" + sync-in-game-weather: false + + world_the_end: + # End hat kein Wetter/Tageslicht – Plugin hier deaktivieren enabled: false location: "Berlin,de" units: "metric" time-format: "24h" - display-actionbar: true - display-weather-icon: false - display-position: "top-right" - padding-right: 150 sync-in-game-weather: false \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index fb0286c..0e2914d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,32 +1,43 @@ name: RealTimeWeather -version: 1.4 +version: 1.5 main: dev.viper.weathertime.WeatherTimeSyncPlugin api-version: 1.21 +authors: [M_Viper] +description: Synchronizes real-time weather and time from OpenWeatherMap with your Minecraft world. commands: wetter: description: Manages real-time weather and time synchronization - usage: / reload | setlocation | query | info | gui | help + usage: / permission: realtimeweather.use weatherforecast: - description: Shows the weather forecast for the player's location + description: Shows the 5-day weather forecast for the player's location usage: / permission: realtimeweather.forecast toggleweather: - description: Toggles the weather display for the current world + description: Toggles the scoreboard weather display for the current world usage: / permission: realtimeweather.toggle permissions: realtimeweather.use: - description: Allows usage of the /wetter command + description: Allows usage of the /wetter command (query, info, gui, mode, help) default: true + children: + realtimeweather.setlocation: true + + realtimeweather.setlocation: + description: Allows setting a custom weather location via /wetter setlocation + default: true + realtimeweather.forecast: description: Allows usage of the /weatherforecast command default: true + realtimeweather.toggle: - description: Allows usage of the /toggleweather command + description: Allows toggling the scoreboard weather display via /toggleweather default: true + realtimeweather.reload: - description: Allows reloading the plugin configuration - default: op + description: Allows reloading the plugin configuration via /wetter reload + default: op \ No newline at end of file