3 Commits
1.4 ... 1.5

Author SHA1 Message Date
affa52d41e Upload pom.xml via GUI 2026-02-28 12:43:32 +00:00
fa599de701 Update from Git Manager GUI 2026-02-28 13:36:51 +01:00
9ace86a856 Upload pom.xml via GUI 2026-02-28 12:36:50 +00:00
4 changed files with 432 additions and 247 deletions

View File

@@ -7,7 +7,7 @@
<groupId>dev.viper</groupId>
<artifactId>RealTimeWeather</artifactId>
<version>1.4</version>
<version>1.5</version>
<packaging>jar</packaging>
<name>RealTimeWeather</name>

View File

@@ -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<String, WorldConfig> worldConfigs = new HashMap<>();
private final Map<String, WeatherTimeData> worldWeatherData = new HashMap<>();
private final Map<String, WeatherTimeData> worldWeatherData = new ConcurrentHashMap<>();
// FIX: Scoreboard standardmäßig AUS nur aktiv wenn der Spieler es in der GUI aktiviert hat
private final Map<UUID, Set<String>> playersWithDisplay = new HashMap<>();
private final Map<UUID, String> playerLocations = new HashMap<>();
private final Map<UUID, Inventory> playerGUIs = new HashMap<>();
private final Map<UUID, Scoreboard> playerScoreboards = new HashMap<>();
private final Map<UUID, DisplayMode> playerDisplayModes = new HashMap<>();
// FIX: Gecachte Wetterdaten pro Spieler (custom location) vermeidet Blocking auf dem Main-Thread
private final Map<UUID, WeatherTimeData> playerWeatherCache = new ConcurrentHashMap<>();
private BukkitAudiences audiences;
private BukkitRunnable weatherUpdateTask;
private BukkitRunnable syncTask;
private final Set<String> processedLocations = new HashSet<>();
private final Set<String> 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());
}
});
@@ -107,25 +116,36 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener {
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<Boolean> doDaylightCycle = (GameRule<Boolean>) 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<Boolean> doWeatherCycle = (GameRule<Boolean>) 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<String> 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<>());
}
}
@@ -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<Block> 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<Snowman> 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<UUID, String> entry : playerLocations.entrySet()) {
UUID playerId = entry.getKey();
String location = entry.getValue();
if (location == null || location.isEmpty()) continue;
List<org.bukkit.Chunk> 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,6 +492,10 @@ 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",
@@ -426,22 +506,30 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener {
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<String> 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
@@ -518,7 +606,6 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener {
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()) +
@@ -527,8 +614,6 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener {
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--);
@@ -544,30 +629,23 @@ 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--);
// 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()) +
(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--);
@@ -575,8 +653,6 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener {
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--);
@@ -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<String> 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<String> 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<String> 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<String> 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;
}
@@ -924,7 +1089,6 @@ public class WeatherTimeSyncPlugin extends JavaPlugin implements Listener {
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<String> 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,23 +1195,14 @@ 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;
}
}

View File

@@ -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: 60300 (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

View File

@@ -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: /<command> reload | setlocation <city,country> | query | info | gui | help
usage: /<command> <reload|setlocation|query|mode|info|gui|help>
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: /<command>
permission: realtimeweather.forecast
toggleweather:
description: Toggles the weather display for the current world
description: Toggles the scoreboard weather display for the current world
usage: /<command>
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
description: Allows reloading the plugin configuration via /wetter reload
default: op