Some refactoring
Some checks failed
/ deploy (push) Has been cancelled

This commit is contained in:
Minecon724 2025-05-26 06:55:31 +02:00
commit 8d1e5d3a3b
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
49 changed files with 1184 additions and 919 deletions

6
.idea/copyright/gpl3.xml generated Normal file
View file

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (c) &amp;#36;originalComment.match(&quot;Copyright \(c\) (\d+)&quot;, 1, &quot;-&quot;, &quot;&amp;#36;today.year&quot;)&amp;#36;today.year RealWeather Authors&#10;RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text." />
<option name="myName" value="gpl3" />
</copyright>
</component>

7
.idea/copyright/profiles_settings.xml generated Normal file
View file

@ -0,0 +1,7 @@
<component name="CopyrightManager">
<settings default="gpl3">
<module2copyright>
<element module="All" copyright="gpl3" />
</module2copyright>
</settings>
</component>

View file

@ -2,7 +2,7 @@ If you're using a firewall, you must allow the following hosts:
- updater:
* git.m724.eu
- weather:
* api.openweathermap.org
* api.open-meteo.com
- thunder:
* ws1.blitzortung.org
* ws7.blitzortung.org

View file

@ -1,73 +0,0 @@
goal: realtime to in game time conversion
There is no need to keep days
minecraft day is 0 - 24000 ticks
where 6000 ticks is noon (peak sun) and 18000 is midnight (peak moon)
irl day is 0 - 86400 seconds
0. s = epoch % 86400 to get seconds since midnight
^
(* scale) here
1. t = s / 72.0 to fit into minecraft day
2. t = t * 20 to convert that to ticks
3. t = t - 6000 to align noon and midnight
this leaves us with negative time, so
4. t = floorMod(t, 24000) to wrap if negative
example:
epoch = 1713593340
0. getting seconds since midnight
s = epoch % 86400
s = 1713593340 % 86400
s = 22140
1. conversion to minecraft day length
gs = s / 72.0
gs = 22140 / 72.0
gs = 307.5
2. to ticks
t = gs * 20
t = 307.5 * 20
t = 6150
3. step 3
t = t - 6000
t = 6150 - 6000
t = 150
4. wrapping
t = floorMod(150, 24000)
t = 150
goal: frequency of time update
t = 72 / scale
t is the period, in ticks of course
to see how many irl seconds a tick represents:
s = 3.6 * scale
(from 1 / (1/72 * 20 * scale))
however, some scales result in fractions
here's how many in game aligned seconds have passed at the end of a real day:
84000 / 72 * scale * floor(72/scale)
for scale 0.99: 83160
so we'll be 14 minutes behind
solution? for now let's warn and update time every tick
to check:
scale * floor(72/scale) == 72
goal: offsetting by player position
t = (longitude / 15) * 1000 * scale
accounting for sunrise and sunset
TODO, idk yet without
update: this is now possible with 0.8.0 api

View file

@ -46,7 +46,7 @@
<dependency>
<groupId>eu.m724</groupId>
<artifactId>wtapi</artifactId>
<version>0.9.2</version>
<version>0.9.3</version>
</dependency>
<dependency>
<groupId>eu.m724</groupId>

View file

@ -1,22 +1,19 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather;
import eu.m724.realweather.mapper.MapperConfig;
import eu.m724.realweather.map.MapConfig;
import eu.m724.realweather.thunder.ThunderConfig;
import eu.m724.realweather.time.TimeConfig;
import eu.m724.realweather.updater.UpdaterConfig;
import eu.m724.realweather.weather.WeatherConfig;
import eu.m724.realweather.weather.WeatherChanger;
// TODO replaces GlobalConstants for configs
public class Configs {
static WeatherConfig weatherConfig;
static TimeConfig timeConfig;
static ThunderConfig thunderConfig;
static MapperConfig mapperConfig;
static UpdaterConfig updaterConfig;
public static WeatherConfig weatherConfig() { return weatherConfig; }
public static TimeConfig timeConfig() { return timeConfig; }
public static ThunderConfig thunderConfig() { return thunderConfig; }
public static MapperConfig mapperConfig() { return mapperConfig; }
public static UpdaterConfig updaterConfig() { return updaterConfig; }
public record Configs(
WeatherChanger weatherConfig,
TimeConfig timeConfig,
ThunderConfig thunderConfig,
MapConfig mapConfig
) {
}

View file

@ -1,18 +1,78 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DebugLogger {
static Logger baseLogger;
static int debugLevel;
public static int getDebugLevel() {
return debugLevel;
private DebugLogger() {}
static Logger logger;
public static void info(String message, Object... format) {
log(Level.INFO, message, format);
}
public static void info(String message, int minDebugLevel, Object... format) {
if (debugLevel >= minDebugLevel)
baseLogger.info(message.formatted(format));
public static void warning(String message, Object... format) {
log(Level.WARNING, message, format);
}
}
public static void severe(String message, Object... format) {
log(Level.SEVERE, message, format);
}
public static void fine(String message, Object... format) {
log(Level.FINE, message, format);
}
public static void finer(String message, Object... format) {
log(Level.FINER, message, format);
}
private static void log(Level level, String message, Object... format) {
if (!logger.isLoggable(level)) {
return;
}
message = message.formatted(format);
if (logger.getLevel().intValue() <= Level.FINE.intValue()) {
message = "[" + getCaller() + "] " + message;
}
if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
// colors text gray (cyan is close to gray)
if (level == Level.FINE) {
message = "\033[38;5;250m" + message + "\033[39m";
} else {
message = "\033[38;5;245m" + message + "\033[39m";
}
level = Level.INFO;
}
logger.log(level, message);
}
private static String getCaller() {
String caller = Thread.currentThread().getStackTrace()[4].getClassName();
if (caller.startsWith("eu.m724.realweather.")) {
caller = caller.substring("eu.m724.realweather.".length());
String[] packages = caller.split("\\.");
caller = IntStream.range(0, packages.length - 1)
.mapToObj(i -> packages[i].substring(0, 2))
.collect(Collectors.joining(".")) + "." + packages[packages.length - 1];
}
return caller;
}
}

View file

@ -1,25 +0,0 @@
package eu.m724.realweather;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.weather.PlayerWeatherCache;
// perhaps replace with a singleton
// TODO actually, remove it altogether
public class GlobalConstants {
static Mapper mapper;
static Plugin plugin;
static PlayerWeatherCache playerWeatherCache;
public static Mapper getMapper() {
return mapper;
}
public static Plugin getPlugin() {
return plugin;
}
public static PlayerWeatherCache getPlayerWeatherCache() {
return playerWeatherCache;
}
}

View file

@ -1,3 +1,8 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather;
import java.io.File;
@ -5,139 +10,213 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
import java.util.logging.Level;
import eu.m724.mstats.MStatsPlugin;
import eu.m724.wtapi.provider.exception.NoSuchProviderException;
import eu.m724.realweather.map.WorldList;
import eu.m724.realweather.updater.UpdateNotifier;
import org.bukkit.configuration.file.YamlConfiguration;
import eu.m724.realweather.commands.AdminCommand;
import eu.m724.realweather.commands.GeoCommand;
import eu.m724.realweather.commands.LocalTimeCommand;
import eu.m724.realweather.commands.LocalWeatherCommand;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.mapper.MapperConfig;
import eu.m724.realweather.map.command.GeoCommand;
import eu.m724.realweather.time.command.LocalTimeCommand;
import eu.m724.realweather.weather.command.LocalWeatherCommand;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.map.MapConfig;
import eu.m724.realweather.thunder.ThunderConfig;
import eu.m724.realweather.thunder.ThunderMaster;
import eu.m724.realweather.time.TimeConfig;
import eu.m724.realweather.time.TimeMaster;
import eu.m724.realweather.updater.PluginUpdater;
import eu.m724.realweather.updater.UpdaterConfig;
import eu.m724.realweather.weather.PlayerWeatherCache;
import eu.m724.realweather.weather.WeatherConfig;
import eu.m724.realweather.weather.WeatherMaster;
import eu.m724.wtapi.provider.exception.ProviderException;
// TODO unmess this too
public class RealWeatherPlugin extends MStatsPlugin {
private Configs configs;
private WorldList worldList;
private WeatherMaster weatherMaster;
private ThunderMaster thunderMaster;
private TimeMaster timeMaster;
private PluginUpdater updater;
private Logger logger;
private ThunderMaster thunderMaster;
private static RealWeatherPlugin INSTANCE;
private CoordinatesLocationConverter coordinatesLocationConverter;
@Override
public void onEnable() {
logger = getLogger();
long start = System.nanoTime();
INSTANCE = this;
File dataFolder = getDataFolder();
File modulesFolder = new File("modules");
modulesFolder.mkdir();
YamlConfiguration configuration,
mapConfiguration, weatherConfiguration,
thunderConfiguration, timeConfiguration;
DebugLogger.info("loading configurations", 1);
boolean firstRun = !new File(dataFolder, "config.yml").exists();
YamlConfiguration configuration;
try {
configuration = getConfig("config.yml");
mapConfiguration = getConfig("map.yml");
weatherConfiguration = getConfig("modules/weather.yml");
thunderConfiguration = getConfig("modules/thunder.yml");
timeConfiguration = getConfig("modules/time.yml");
} catch (IOException e) {
logger.severe("Failed to load config!");
throw new RuntimeException(e);
DebugLogger.severe("Failed to load configuration:");
DebugLogger.severe(" " + e);
getServer().getPluginManager().disablePlugin(this);
return;
}
DebugLogger.logger = getLogger();
if (configuration.getBoolean("debug")) {
getLogger().setLevel(Level.FINEST);
DebugLogger.warning("Debug harms performance");
}
if (firstRun) {
logger.warning("This is your first time running this plugin.");
logger.warning("Please *shut down* the server, review the config files (enable modules, enter your API keys, etc.)");
logger.warning("Don't forget to enable the plugin in config.yml");
DebugLogger.warning("This is your first time running this plugin.");
DebugLogger.warning("Please *shut down* the server, review the config files (enable modules, enter API keys, etc.)");
DebugLogger.warning("Don't forget to enable the plugin in config.yml");
getServer().getPluginManager().disablePlugin(this);
return;
}
DebugLogger.baseLogger = logger;
DebugLogger.debugLevel = configuration.getInt("debug");
if (!configuration.getBoolean("enabled")) {
logger.warning("Plugin disabled by administrator. Enable it in config.yml");
if (configuration.getBoolean("disabled")) {
DebugLogger.warning("Plugin disabled per config. Enable it in config.yml");
getServer().getPluginManager().disablePlugin(this);
return;
}
GlobalConstants.plugin = this;
GlobalConstants.playerWeatherCache = new PlayerWeatherCache();
DebugLogger.info("loading mapper", 1);
Configs.mapperConfig = MapperConfig.fromConfiguration(mapConfiguration);
GlobalConstants.mapper = new Mapper();
GlobalConstants.mapper.registerEvents(this);
try {
DebugLogger.info("loading weather", 1);
Configs.weatherConfig = WeatherConfig.fromConfiguration(weatherConfiguration);
weatherMaster = new WeatherMaster();
weatherMaster.init(this);
DebugLogger.info("loading thunder", 1);
Configs.thunderConfig = ThunderConfig.fromConfiguration(thunderConfiguration);
thunderMaster = new ThunderMaster();
thunderMaster.init(this);
DebugLogger.info("loading time", 1);
Configs.timeConfig = TimeConfig.fromConfiguration(timeConfiguration);
timeMaster = new TimeMaster();
timeMaster.init();
loadMapModules();
} catch (Exception e) {
DebugLogger.severe("Failed to load the Map module:");
DebugLogger.severe(" " + e);
Configs.updaterConfig = UpdaterConfig.fromConfiguration(configuration.getConfigurationSection("updater"));
updater = PluginUpdater.build(this, this.getFile());
//updater.init();
} catch (NoSuchProviderException e) {
logger.severe("There's an error in your config:");
logger.severe(" " + e.getMessage());
getServer().getPluginManager().disablePlugin(this);
return;
} catch (ProviderException e) {
logger.severe("Couldn't initialize provider!");
logger.severe("Possible causes:");
logger.severe("1. Your API key is invalid, or was added too recently");
logger.severe("2. The provider or your internet is down");
e.printStackTrace();
getServer().getPluginManager().disablePlugin(this);
return;
}
getCommand("rwadmin").setExecutor(new AdminCommand(updater, thunderMaster));
getCommand("geo").setExecutor(new GeoCommand());
try {
loadWeatherModule();
} catch (Exception e) {
DebugLogger.severe("Failed to load the Weather module:");
DebugLogger.severe(" " + e);
if (Configs.timeConfig.enabled())
getCommand("localtime").setExecutor(new LocalTimeCommand(timeMaster.getTimeConverter()));
if (Configs.weatherConfig.enabled()) {
getCommand("localweather").setExecutor(new LocalWeatherCommand());
getServer().getPluginManager().disablePlugin(this);
return;
}
try {
loadThunderModule();
} catch (Exception e) {
DebugLogger.severe("Failed to load the Thunder module:");
DebugLogger.severe(" " + e);
getServer().getPluginManager().disablePlugin(this);
return;
}
try {
loadTimeModule();
} catch (Exception e) {
DebugLogger.severe("Failed to load the Time module:");
DebugLogger.severe(" " + e);
getServer().getPluginManager().disablePlugin(this);
return;
}
DebugLogger.fine("Loading Updater");
PluginUpdater updater = PluginUpdater.build(this.getFile(), configuration.getString("updater.channel"));
if (configuration.getBoolean("updater.notify")) {
UpdateNotifier updateNotifier = new UpdateNotifier(updater);
updateNotifier.register();
}
DebugLogger.fine("Done loading Updater");
getCommand("rwadmin").setExecutor(new AdminCommand(this, updater));
getCommand("geo").setExecutor(new GeoCommand(coordinatesLocationConverter));
mStats(2);
DebugLogger.info("ended loading", 1);
DebugLogger.fine("Plugin enabled. Took %.3f milliseconds", (System.nanoTime() - start) / 1000000.0);
}
// TODO repeating here!
private void loadMapModules() throws IOException {
DebugLogger.fine("Loading the Map modules");
YamlConfiguration yamlConfiguration = getConfig("map.yml");
MapConfig mapConfig = MapConfig.fromConfiguration(yamlConfiguration);
this.coordinatesLocationConverter = new CoordinatesLocationConverter(mapConfig);
this.worldList = new WorldList(mapConfig.worldNames(), mapConfig.worldNamesIsBlacklist());
getServer().getPluginManager().registerEvents(worldList, this);
DebugLogger.finer("Done loading Map modules");
}
private void loadWeatherModule() throws IOException {
DebugLogger.fine("Loading the Weather module");
YamlConfiguration yamlConfiguration = getConfig("modules/weather.yml");
WeatherConfig weatherConfig = WeatherConfig.fromConfiguration(yamlConfiguration);
if (!weatherConfig.enabled()) {
DebugLogger.finer("Weather module disabled per config");
return;
}
this.weatherMaster = new WeatherMaster(this, weatherConfig);
weatherMaster.init();
getCommand("localweather").setExecutor(new LocalWeatherCommand(weatherMaster.getPlayerWeatherStore()));
DebugLogger.finer("Enabled Weather module");
}
private void loadThunderModule() throws IOException {
DebugLogger.fine("Loading the Thunder module");
YamlConfiguration yamlConfiguration = getConfig("modules/thunder.yml");
ThunderConfig thunderConfig = ThunderConfig.fromConfiguration(yamlConfiguration);
if (!thunderConfig.enabled()) {
DebugLogger.finer("Thunder module disabled per config");
return;
}
this.thunderMaster = new ThunderMaster(this, thunderConfig);
thunderMaster.init();
DebugLogger.finer("Enabled Thunder module");
}
private void loadTimeModule() throws IOException {
DebugLogger.fine("Loading the Time module");
YamlConfiguration yamlConfiguration = getConfig("modules/time.yml");
TimeConfig timeConfig = TimeConfig.fromConfiguration(yamlConfiguration);
if (!timeConfig.enabled()) {
DebugLogger.finer("Time module disabled per config");
return;
}
this.timeMaster = new TimeMaster(this, timeConfig);
timeMaster.init();
getCommand("localtime").setExecutor(new LocalTimeCommand(timeMaster, coordinatesLocationConverter));
DebugLogger.finer("Enabled Time module");
}
public YamlConfiguration getConfig(String configFilePath) throws IOException {
@ -145,15 +224,50 @@ public class RealWeatherPlugin extends MStatsPlugin {
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
if (!configFile.exists()) {
final InputStream defConfigStream = getResource(configFilePath);
if (defConfigStream == null)
return null;
config = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, StandardCharsets.UTF_8));
try (InputStream defConfigStream = getResource(configFilePath)) {
if (defConfigStream == null)
return null;
config = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, StandardCharsets.UTF_8));
}
config.save(configFile);
}
return config;
}
/**
* Gets the instance of RealWeather plugin.
*
* @return The instance of RealWeather plugin.
*/
public static RealWeatherPlugin getInstance() {
return INSTANCE;
}
/**
* Gets the coordinates to location converter.
*
* @return The coordinates to location converter.
*/
public CoordinatesLocationConverter getCoordinatesLocationConverter() {
return coordinatesLocationConverter;
}
public WeatherMaster getWeatherMaster() {
return weatherMaster;
}
public TimeMaster getTimeMaster() {
return timeMaster;
}
public ThunderMaster getThunderMaster() {
return thunderMaster;
}
public WorldList getWorldList() {
return worldList;
}
}

View file

@ -1,3 +1,8 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.api.weather;
import eu.m724.wtapi.object.Weather;

View file

@ -1,3 +1,8 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.api.weather;
import eu.m724.wtapi.object.Weather;

View file

@ -1,3 +1,8 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.api.weather;
import eu.m724.wtapi.object.Weather;

View file

@ -1,67 +1,80 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.commands;
import eu.m724.realweather.Configs;
import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.time.TimeMaster;
import eu.m724.realweather.updater.command.UpdateCommand;
import eu.m724.realweather.weather.WeatherMaster;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.MapperConfig;
import eu.m724.realweather.thunder.ThunderConfig;
import eu.m724.realweather.thunder.ThunderMaster;
import eu.m724.realweather.time.TimeConfig;
import eu.m724.realweather.updater.PluginUpdater;
import eu.m724.realweather.weather.WeatherConfig;
import net.md_5.bungee.api.ChatColor;
// TODO unmess this all
public class AdminCommand implements CommandExecutor {
private final RealWeatherPlugin plugin;
private final UpdateCommand updateCommand;
private final Plugin plugin = GlobalConstants.getPlugin();
private final WeatherConfig weatherConfig = Configs.weatherConfig();
private final TimeConfig timeConfig = Configs.timeConfig();
private final ThunderConfig thunderConfig = Configs.thunderConfig();
private final MapperConfig mapperConfig = Configs.mapperConfig();
private final WeatherMaster weatherMaster;
private final TimeMaster timeMaster;
private final ThunderMaster thunderMaster;
private final CoordinatesLocationConverter coordinatesLocationConverter;
public AdminCommand(PluginUpdater updater, ThunderMaster thunderMaster) {
public AdminCommand(RealWeatherPlugin plugin, PluginUpdater updater) {
this.plugin = plugin;
this.updateCommand = new UpdateCommand(updater);
this.thunderMaster = thunderMaster;
this.weatherMaster = plugin.getWeatherMaster();
this.timeMaster = plugin.getTimeMaster();
this.thunderMaster = plugin.getThunderMaster();
this.coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0 && args[0].equals("update")) {
return updateCommand.onCommand(sender, command, label, args);
updateCommand.updateCommand(sender, args);
return true;
}
colorize(sender, "\n&eRealWeather %s\n\n", plugin.getDescription().getVersion().replace("-SNAPSHOT", "&c-SNAPSHOT"));
colorize(sender, "&6Coordinate scale: &b%d, %d &7blocks / deg", mapperConfig.scaleLatitude, mapperConfig.scaleLongitude);
colorize(sender, "&6Coordinate scale: &b%f, %f &7blocks / deg", coordinatesLocationConverter.getScaleLatitude(), coordinatesLocationConverter.getScaleLongitude());
colorize(sender, "&6Static point: &b%f, %f &7lat, lon", coordinatesLocationConverter.getStaticPoint().latitude(), coordinatesLocationConverter.getStaticPoint().longitude());
sender.sendMessage("");
colorize(sender, "\n&6Weather: %s", weatherConfig.enabled() ? (weatherConfig.dynamic() ? "&aYes, dynamic" : "&aYes, static") : "&cDisabled");
if (weatherConfig.enabled()) {
colorize(sender, " &6Provider: &b%s", weatherConfig.provider());
if (weatherMaster != null) {
colorize(sender, "&6Weather: %s", weatherMaster.isDynamic() ? "&aYes, dynamic" : "&aYes, static");
colorize(sender, " &6Provider: &b%s", weatherMaster.getProviderName());
colorize(sender, " &6/localweather to see current weather");
} else {
colorize(sender, "&6Weather: %s", "&cDisabled");
}
colorize(sender, "\n&6Time: %s", timeConfig.enabled() ? (timeConfig.dynamic() ? "&aYes, dynamic" : "&aYes, static") : "&cDisabled");
if (timeConfig.enabled()) {
colorize(sender, " &6Scale: &b%s&7x", timeConfig.scale());
sender.sendMessage("");
if (timeMaster != null) {
colorize(sender, "&6Time: %s", timeMaster.isDynamic() ? "&aYes, dynamic" : "&aYes, static");
colorize(sender, " &6Scale: &b%s&7x", timeMaster.getTimeConverter().getScale());
colorize(sender, " &6/localtime to see current time");
} else {
colorize(sender, "&6Time: &cDisabled");
}
colorize(sender, "\n&6Thunder: %s", thunderConfig.enabled() ? "&aYes, dynamic" : "&cDisabled");
if (thunderConfig.enabled()) {
colorize(sender, " &6Provider: &b%s", thunderConfig.provider());
colorize(sender, " &6Refresh period: &b%d &7ticks", thunderConfig.refreshPeriod());
sender.sendMessage("");
if (thunderMaster != null) {
colorize(sender, "&6Thunder: &aYes, dynamic");
colorize(sender, " &6Provider: &b%s", thunderMaster.getProviderName());
colorize(sender, " &6API latency: &b%d&7ms", thunderMaster.getLatency());
} else {
colorize(sender, "&6Thunder: &cDisabled");
}
return true;

View file

@ -1,77 +0,0 @@
package eu.m724.realweather.commands;
import java.nio.file.NoSuchFileException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import eu.m724.jarupdater.updater.Updater;
import eu.m724.jarupdater.object.Version;
/**
* not actually a command but deserves a separate file
*/
public class UpdateCommand {
private final Updater updater;
public UpdateCommand(Updater updater) {
this.updater = updater;
}
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("realweather.admin.update")) return false;
sender.sendMessage("Please wait");
CompletableFuture<Version> latestFuture = updater.getLatestVersion();
if (args.length == 0) {
latestFuture.thenAccept(metadata -> {
if (metadata != null) {
sender.sendMessage("An update is available!");
sender.sendMessage("RealWeather %s released %s".formatted(metadata.getLabel(), formatDate(metadata.getTimestamp())));
sender.sendMessage("To download: /rwadmin update download");
} else {
sender.sendMessage("No new updates"); // TODO color
}
});
} else {
String action = args[1]; // remember this function is proxied
if (action.equals("download")) {
sender.sendMessage("Started download");
updater.downloadLatestVersion().handle((file, ex) -> {
sender.sendMessage("Download failed. See console for details.");
ex.printStackTrace();
return null;
}).thenAccept(file -> {
if (file != null)
sender.sendMessage("Download finished, install with /rwadmin update install");
});
} else if (action.equals("install")) {
try {
updater.installLatestVersion().handle((v, ex) -> {
sender.sendMessage("Install failed. See console for details.");
ex.printStackTrace();
return null;
}).thenAccept(v -> {
sender.sendMessage("Installation completed, restart server to apply");
});
} catch (NoSuchFileException e) {
sender.sendMessage("Download the update first");
}
} else return false;
}
return true;
}
private String formatDate(long timestamp) { // TODO move this
return DateTimeFormatter.ofPattern("dd.MM.yyyy").format(Instant.ofEpochSecond(timestamp));
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.map;
import org.bukkit.Location;
import org.bukkit.World;
import eu.m724.wtapi.object.Coordinates;
public class CoordinatesLocationConverter {
private final double scaleLatitude;
private final double scaleLongitude;
private final Coordinates staticPoint;
public CoordinatesLocationConverter(MapConfig config) {
this.scaleLatitude = config.scaleLatitude();
this.scaleLongitude = config.scaleLongitude();
this.staticPoint = config.point();
}
/**
* Converts a {@link Location} to {@link Coordinates}<br>
* The result is scaled and wrapped
*
* @param location the location to convert
* @return the coordinates
*/
public Coordinates locationToCoordinates(Location location) {
// it's <-90, 90> (inclusive), but there's no point to make it that way here, because it's easier, and nice precision is enough
double latitude = (-location.getZ() / scaleLatitude) % 90;
// here it's <-180, 180) so it's correct, and we don't need excuses
double longitude = (location.getX() / scaleLongitude) % 180;
return new Coordinates(latitude, longitude);
}
/**
* Converts {@link Coordinates} to a {@link Location}<br>
* The result is scaled, but not wrapped to world border or anything
*
* @param world the world of the location
* @param coordinates the coordinates to convert
* @return the location in {@code world}
*/
public Location coordinatesToLocation(World world, Coordinates coordinates) {
double x = coordinates.longitude() * scaleLongitude;
double z = -coordinates.latitude() * scaleLatitude;
return new Location(world, x, 0, z);
}
public Coordinates getStaticPoint() {
return staticPoint;
}
public double getScaleLatitude() {
return scaleLatitude;
}
public double getScaleLongitude() {
return scaleLongitude;
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.map;
import java.util.List;
import org.bukkit.configuration.ConfigurationSection;
import eu.m724.wtapi.object.Coordinates;
public record MapConfig(
List<String> worldNames,
boolean worldNamesIsBlacklist,
double scaleLatitude,
double scaleLongitude,
Coordinates point
) {
public static MapConfig fromConfiguration(ConfigurationSection configuration) {
List<String> worldNames = configuration.getStringList("worlds");
boolean worldBlacklist = configuration.getBoolean("isBlacklist");
double scaleLatitude = configuration.getDouble("dimensions.latitude");
double scaleLongitude = configuration.getDouble("dimensions.longitude");
Coordinates point = new Coordinates(
configuration.getDouble("point.latitude"),
configuration.getDouble("point.longitude")
);
return new MapConfig(worldNames, worldBlacklist, scaleLatitude, scaleLongitude, point);
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.map;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class WorldList implements Listener {
private final List<String> worldNames;
private final boolean isBlacklist;
private final List<World> worlds = new ArrayList<>();
public WorldList(List<String> worldNames, boolean isBlacklist) {
this.worldNames = worldNames;
this.isBlacklist = isBlacklist;
}
@EventHandler
void onWorldLoad(WorldLoadEvent e) {
if (worldNames.contains(e.getWorld().getName()) ^ isBlacklist) {
worlds.add(e.getWorld());
}
}
@EventHandler
void onWorldUnload(WorldUnloadEvent e) {
worlds.remove(e.getWorld());
}
public List<World> getIncludedWorlds() {
return Collections.unmodifiableList(this.worlds);
}
public boolean isWorldIncluded(World world) {
return worlds.contains(world);
}
}

View file

@ -1,4 +1,9 @@
package eu.m724.realweather.commands;
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.map.command;
import org.bukkit.Location;
import org.bukkit.command.Command;
@ -6,31 +11,29 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.wtapi.object.Coordinates;
import net.md_5.bungee.api.ChatColor;
public class GeoCommand implements CommandExecutor {
//private final PlayerWeatherCache playerWeatherCache = GlobalConstants.getPlayerWeatherCache();
private final Mapper mapper = GlobalConstants.getMapper();
private final CoordinatesLocationConverter coordinatesLocationConverter;
@Override
public GeoCommand(CoordinatesLocationConverter coordinatesLocationConverter) {
this.coordinatesLocationConverter = coordinatesLocationConverter;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Player player = sender instanceof Player ? (Player) sender : null;
if (args.length == 0) {
if (player != null) {
Location location = player.getLocation();
Coordinates coordinates = mapper.locationToCoordinates(location);
//Weather weather = playerWeatherCache.getWeather(player);
//String address = formatAddress(weather);
Coordinates coordinates = coordinatesLocationConverter.locationToCoordinates(location);
colorize(player, "");
colorize(player, "&6Geolocation: &b%f&7, &b%f &7(lat, lon)", coordinates.latitude(), coordinates.longitude());
colorize(player, "&7In-game Position: &3%f&8, &3%f &8(x, z)", location.getX(), location.getZ());
//colorize(player, "&7City: &3%s", address);
colorize(player, "");
} else {
sender.sendMessage("You can't run this command without arguments as console");
@ -49,12 +52,12 @@ public class GeoCommand implements CommandExecutor {
}
Location location = new Location(null, x, 0, z);
Coordinates coordinates = mapper.locationToCoordinates(location);
Coordinates coordinates = coordinatesLocationConverter.locationToCoordinates(location);
colorize(sender, "");
colorize(sender, "&6In-game Position: &b%f&7, &b%f &7(x, z)", location.getX(), location.getZ());
colorize(sender, "&7Geolocation: &3%f&8, &3%f &8(lat, lon)", coordinates.latitude(), coordinates.longitude());
colorize(sender, "&7Input interpreted as position, because you separated it with a space");
colorize(sender, "&6Geolocation: &b%f&8, &b%f &7(lat, lon)", coordinates.latitude(), coordinates.longitude());
colorize(sender, "&7In-game Position: &3%f&7, &3%f &8(x, z)", location.getX(), location.getZ());
colorize(sender, "&7Input interpreted as position, space used as separator");
colorize(sender, "");
} else {
double latitude, longitude;
@ -69,12 +72,12 @@ public class GeoCommand implements CommandExecutor {
}
Coordinates coordinates = new Coordinates(latitude, longitude);
Location location = mapper.coordinatesToLocation(null, coordinates);
Location location = coordinatesLocationConverter.coordinatesToLocation(null, coordinates);
colorize(sender, "");
colorize(sender, "&6In-game Position: &b%f&7, &b%f &7(x, z)", location.getX(), location.getZ());
colorize(sender, "&7Geolocation: &3%f&8, &3%f &8(lat, lon)", coordinates.latitude(), coordinates.longitude());
colorize(sender, "&7Input interpreted as geolocation, because you separated with a comma");
colorize(sender, "&7Input interpreted as geolocation, comma used as separator");
colorize(sender, "");
}

View file

@ -1,103 +0,0 @@
package eu.m724.realweather.mapper;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import eu.m724.realweather.Configs;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.plugin.Plugin;
import eu.m724.wtapi.object.Coordinates;
public class Mapper {
private final MapperConfig config = Configs.mapperConfig();
private final List<World> worlds = new ArrayList<>();
private final List<Consumer<World>> worldLoadConsumers = new ArrayList<>();
private final List<Consumer<World>> worldUnloadConsumers = new ArrayList<>();
// TODO game rules, I think I meant handling by this class
/**
* Registers a consumer which will be called on world load
* @param consumer the consumer which will be called on world load
*/
public void registerWorldLoadConsumer(Consumer<World> consumer) {
this.worldLoadConsumers.add(consumer);
}
/**
* Registers a consumer which will be called on world unload
* @param consumer the consumer which will be called on world unload
*/
public void registerWorldUnloadConsumer(Consumer<World> consumer) {
this.worldUnloadConsumers.add(consumer);
}
/**
* Registers events handled by mapper.<br>
* This should be called once on plugin load.
* @param plugin the plugin to register events under
*/
public void registerEvents(Plugin plugin) {
plugin.getServer().getPluginManager().registerEvents(new MapperEventHandler(this), plugin);
}
/**
* Converts a {@link Location} to {@link Coordinates}<br>
* The result is scaled and wrapped
*
* @param location the location to convert
* @return the coordinates
*/
public Coordinates locationToCoordinates(Location location) {
// it's <-90, 90> (inclusive), but there's no point to make it that way here, because it's easier, and nice precision is enough
double latitude = (-location.getZ() / config.scaleLatitude) % 90;
// here it's <-180, 180) so it's correct, and we don't need excuses
double longitude = (location.getX() / config.scaleLongitude) % 180;
return new Coordinates(latitude, longitude);
}
/**
* Converts {@link Coordinates} to a {@link Location}<br>
* The result is scaled, but not wrapped to world border or anything
*
* @param world the world of the location
* @param coordinates the coordinates to convert
* @return the location in {@code world}
*/
public Location coordinatesToLocation(World world, Coordinates coordinates) {
double x = coordinates.longitude() * config.scaleLongitude;
double z = -coordinates.latitude() * config.scaleLatitude;
return new Location(world, x, 0, z);
}
public Coordinates getPoint() {
return config.point;
}
public List<World> getWorlds() {
return this.worlds;
}
boolean loadWorld(World world) {
boolean loaded = config.worlds.contains(world.getName()) ^ config.worldBlacklist;
if (loaded) {
worlds.add(world);
worldLoadConsumers.forEach(consumer -> consumer.accept(world));
}
return loaded;
}
void unloadWorld(World world) {
if (worlds.remove(world)) {
worldUnloadConsumers.forEach(consumer -> consumer.accept(world));
}
}
}

View file

@ -1,34 +0,0 @@
package eu.m724.realweather.mapper;
import java.util.List;
import org.bukkit.configuration.ConfigurationSection;
import eu.m724.wtapi.object.Coordinates;
public class MapperConfig {
public boolean worldBlacklist;
public List<String> worlds;
public double scaleLatitude;
public double scaleLongitude;
public Coordinates point;
public static MapperConfig fromConfiguration(ConfigurationSection configuration) {
MapperConfig mapperConfig = new MapperConfig();
mapperConfig.worldBlacklist = configuration.getBoolean("worldBlacklist");
mapperConfig.worlds = configuration.getStringList("worlds");
mapperConfig.scaleLatitude = configuration.getDouble("dimensions.latitude");
mapperConfig.scaleLongitude = configuration.getDouble("dimensions.longitude");
mapperConfig.point = new Coordinates(
configuration.getDouble("point.latitude"),
configuration.getDouble("point.longitude")
);
return mapperConfig;
}
}

View file

@ -1,24 +0,0 @@
package eu.m724.realweather.mapper;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
public class MapperEventHandler implements Listener {
private final Mapper mapper;
public MapperEventHandler(Mapper mapper) {
this.mapper = mapper;
}
@EventHandler
public void onWorldLoad(WorldLoadEvent e) {
mapper.loadWorld(e.getWorld());
}
@EventHandler
public void onWorldUnload(WorldUnloadEvent e) {
mapper.unloadWorld(e.getWorld());
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.thunder;
import eu.m724.wtapi.provider.thunder.TimedStrike;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Called on a lightning strike.
*/
public class AsyncLightningStrikeEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final TimedStrike timedStrike;
private boolean cancelled;
public AsyncLightningStrikeEvent(TimedStrike timedStrike) {
super(true);
this.timedStrike = timedStrike;
}
public TimedStrike getTimedStrike() {
return timedStrike;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.thunder;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.map.WorldList;
import eu.m724.wtapi.provider.thunder.TimedStrike;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
public class LightningListener implements Listener {
private final CoordinatesLocationConverter coordinatesLocationConverter;
private final WorldList worldList;
public LightningListener(CoordinatesLocationConverter coordinatesLocationConverter, WorldList worldList) {
this.coordinatesLocationConverter = coordinatesLocationConverter;
this.worldList = worldList;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onLightningStrike(AsyncLightningStrikeEvent event) {
if (event.isCancelled()) return;
TimedStrike strike = event.getTimedStrike();
DebugLogger.finer("Strike: %f %f", strike.coordinates().latitude(), strike.coordinates().longitude());
worldList.getIncludedWorlds().forEach(w -> {
Location location = coordinatesLocationConverter.coordinatesToLocation(w, strike.coordinates());
DebugLogger.finer("In %s that converts to: %d %d", w.getName(), location.getBlockX(), location.getBlockZ());
// World#isLoaded, Chunk#isLoaded and probably others using Chunk, load the chunk and always return true
if (w.isChunkLoaded(location.getBlockX() / 16, location.getBlockZ() / 16)) {
location.setY(w.getHighestBlockYAt(location) + 1);
w.strikeLightning(location);
DebugLogger.finer("Spawned lightning in %s on y level %d", w.getName(), location.getBlockY());
}
});
}
}

View file

@ -1,34 +0,0 @@
package eu.m724.realweather.thunder;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.wtapi.provider.thunder.TimedStrike;
import org.bukkit.Location;
import org.bukkit.scheduler.BukkitRunnable;
public class StrikeTask extends BukkitRunnable {
private final Mapper mapper = GlobalConstants.getMapper();
private final TimedStrike strike;
public StrikeTask(TimedStrike strike) {
this.strike = strike;
}
@Override
public void run() {
DebugLogger.info("strike: %f %f", 2, strike.coordinates().latitude(), strike.coordinates().longitude());
mapper.getWorlds().forEach(w -> {
Location location = mapper.coordinatesToLocation(w, strike.coordinates());
DebugLogger.info("in %s that converts to: %d %d", 2, w.getName(), location.getBlockX(), location.getBlockZ());
// World#isLoaded, Chunk#isLoaded and probably others using Chunk, load the chunk and always return true
if (w.isChunkLoaded(location.getBlockX() / 16, location.getBlockZ() / 16)) {
location.setY(w.getHighestBlockYAt(location) + 1);
w.strikeLightning(location);
DebugLogger.info("spawned lightning in %s on y level %d", 2, w.getName(), location.getBlockY());
}
});
}
}

View file

@ -1,23 +1,29 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.thunder;
import org.bukkit.configuration.ConfigurationSection;
/**
* Configuration of the thunder module
*
* @param enabled is thunder module enabled
* @param provider The provider name, may or may not exist, if it doesn't, an error is thrown later
* @param refreshPeriod how often probe for strikes, in ticks
* @param enabled Whether the thunder module is enabled
* @param provider The provider name
* @param apiKey API key for the provider, null if not necessary
*/
public record ThunderConfig(
boolean enabled,
String provider,
int refreshPeriod
String apiKey
) {
public static ThunderConfig fromConfiguration(ConfigurationSection configuration) {
return new ThunderConfig(
configuration.getBoolean("enabled"),
configuration.getString("provider"),
configuration.getInt("refreshPeriod")
configuration.getString("apiKey")
);
}
}

View file

@ -1,43 +1,67 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.thunder;
import eu.m724.realweather.Configs;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.wtapi.provider.exception.NoSuchProviderException;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.wtapi.provider.exception.NoSuchProviderException;
import eu.m724.wtapi.provider.Providers;
import eu.m724.wtapi.provider.exception.ProviderException;
import eu.m724.wtapi.provider.thunder.ThunderProvider;
public class ThunderMaster {
private final Plugin plugin = GlobalConstants.getPlugin();
private final ThunderConfig config = Configs.thunderConfig();
private final Mapper mapper = GlobalConstants.getMapper();
private final RealWeatherPlugin plugin;
private final String providerName;
private final String apiKey;
private ThunderProvider provider;
/**
public ThunderMaster(RealWeatherPlugin plugin, ThunderConfig config) {
this.plugin = plugin;
this.providerName = config.provider();
this.apiKey = config.apiKey();
}
/**
* initializes, tests and starts
*
* @throws ProviderException if provider initialization failed
* @throws NoSuchProviderException config issue
*/
public void init(Plugin plugin) throws ProviderException, NoSuchProviderException {
if (!config.enabled())
return;
provider = Providers.getThunderProvider(config.provider(), null);
public void init() throws ProviderException, NoSuchProviderException {
provider = Providers.getThunderProvider(providerName, apiKey);
provider.registerStrikeConsumer(strike -> new StrikeTask(strike).runTaskLaterAsynchronously(plugin, 0));
// TODO is this good? Probably not
provider.registerStrikeConsumer(strike -> {
plugin.getServer().getPluginManager().callEvent(
new AsyncLightningStrikeEvent(strike)
);
});
provider.registerEventConsumer(event -> {
DebugLogger.fine("Thunder provider says: %s", event.message());
if (event.exception() != null) {
DebugLogger.severe("Thunder provider exception: %s", event.message());
DebugLogger.severe(" " + event.exception());
}
});
provider.start();
DebugLogger.info("thunderprovider started", 3);
DebugLogger.info("thunder loaded", 1);
DebugLogger.finer("Done initializing");
}
public long getLatency() {
return provider.getLatency();
}
// TODO should this be exposed?
public String getProviderName() {
return providerName;
}
}

View file

@ -1,37 +1,45 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.time;
import eu.m724.realweather.map.WorldList;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.wtapi.object.Coordinates;
public class AsyncPlayerTimeTask extends BukkitRunnable {
private final Server server = GlobalConstants.getPlugin().getServer();
private final Mapper mapper = GlobalConstants.getMapper();
private final TimeConverter timeConverter;
private final Server server;
private final CoordinatesLocationConverter coordinatesLocationConverter;
private final WorldList worldList;
AsyncPlayerTimeTask(TimeConverter timeConverter) {
AsyncPlayerTimeTask(TimeConverter timeConverter, Server server, CoordinatesLocationConverter coordinatesLocationConverter, WorldList worldList) {
this.timeConverter = timeConverter;
this.server = server;
this.coordinatesLocationConverter = coordinatesLocationConverter;
this.worldList = worldList;
}
@Override
public void run() {
for (Player player : server.getOnlinePlayers()) {
if (!player.hasPermission("realweather.dynamic")) continue;
if (!mapper.getWorlds().contains(player.getWorld())) continue;
if (!worldList.isWorldIncluded(player.getWorld())) continue;
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation());
Coordinates coordinates = coordinatesLocationConverter.locationToCoordinates(player.getLocation());
long time = timeConverter.calculateZoneOffset(coordinates.longitude());
long ticks = timeConverter.millisToTicks(time);
player.setPlayerTime(ticks, true);
DebugLogger.info("Time for %s: %d", 2, player.getName(), time);
DebugLogger.finer("Time for %s: %d", player.getName(), time);
}
}
}

View file

@ -1,23 +1,30 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.time;
import eu.m724.realweather.map.WorldList;
import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.CoordinatesLocationConverter;
/**
* This does world time, player time is basically offset of this, like timezone +0
*/
public class SyncTimeUpdateTask extends BukkitRunnable {
private final Mapper mapper = GlobalConstants.getMapper();
private final TimeConverter timeConverter;
private final WorldList worldList;
private final long zoneOffset;
SyncTimeUpdateTask(TimeConverter timeConverter, boolean dynamic) {
SyncTimeUpdateTask(TimeConverter timeConverter, WorldList worldList, CoordinatesLocationConverter coordinatesLocationConverter, boolean dynamic) {
this.timeConverter = timeConverter;
this.zoneOffset = !dynamic ? timeConverter.calculateZoneOffset(mapper.getPoint().longitude()) : 0;
this.worldList = worldList;
this.zoneOffset = !dynamic ? timeConverter.calculateZoneOffset(coordinatesLocationConverter.getStaticPoint().longitude()) : 0;
}
@Override
@ -27,10 +34,9 @@ public class SyncTimeUpdateTask extends BukkitRunnable {
long ticks = timeConverter.millisToTicks(time + zoneOffset);
DebugLogger.info("Updating time: %d", 2, ticks);
DebugLogger.finer("Updating world time: %d", ticks);
mapper.getWorlds().forEach(world -> world.setFullTime(ticks));
// TODO add world handlers to mapper and don't calculate time each run
worldList.getIncludedWorlds().forEach(world -> world.setFullTime(ticks));
// TODO don't calculate time each run
}
}

View file

@ -1,3 +1,8 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.time;
import org.bukkit.configuration.ConfigurationSection;

View file

@ -1,15 +1,24 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.time;
public class TimeConverter {
public final double scale;
private final double scale;
public TimeConverter(double scale) {
if (scale <= 0.0) {
throw new IllegalArgumentException("Scale must be greater than 0.0");
}
this.scale = scale;
}
/**
* Divides time by predefined scale<br>
* ...slowing it down
* ...slowing it down (or speeding up)
*
* @param time unix milliseconds
* @return scaled unix milliseconds
@ -48,4 +57,8 @@ public class TimeConverter {
public long calculateZoneOffset(double longitude) {
return (long) (longitude * 240000);
}
public double getScale() {
return scale;
}
}

View file

@ -1,45 +1,62 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.time;
import eu.m724.realweather.Configs;
import org.bukkit.GameRule;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.map.WorldList;
public class TimeMaster {
private final Mapper mapper = GlobalConstants.getMapper();
private final Plugin plugin = GlobalConstants.getPlugin();
private final TimeConfig timeConfig = Configs.timeConfig();
private final RealWeatherPlugin plugin;
private final TimeConverter timeConverter;
// TODO I don't want to initialize this here
private final TimeConverter timeConverter = new TimeConverter(timeConfig.scale());
private final boolean dynamic;
public TimeMaster(RealWeatherPlugin plugin, TimeConfig timeConfig) {
this.plugin = plugin;
this.timeConverter = new TimeConverter(timeConfig.scale());
this.dynamic = timeConfig.dynamic();
}
public void init() {
long period = timeConverter.calculateUpdatePeriod();
DebugLogger.fine("Updates every %d ticks", period);
if (timeConverter.getScale() * period != 72.0) {
// TODO does it matter in practice?
DebugLogger.warning("Time scale is not optimal. Time might be inaccurate or choppy.");
}
WorldList worldList = plugin.getWorldList();
CoordinatesLocationConverter coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
new SyncTimeUpdateTask(timeConverter, worldList, coordinatesLocationConverter, dynamic)
.runTaskTimer(plugin, 0, period);
if (dynamic) {
// Not period because this is offset
new AsyncPlayerTimeTask(timeConverter, plugin.getServer(), coordinatesLocationConverter, worldList)
.runTaskTimerAsynchronously(plugin, 0, 5 * 20); // 5 seconds
}
// TODO replace that
// coordinatesLocationConverter.registerWorldLoadConsumer(world -> world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false));
// coordinatesLocationConverter.registerWorldUnloadConsumer(world -> world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, true));
DebugLogger.finer("Done initializing");
}
// TODO this is only used once
public TimeConverter getTimeConverter() {
return timeConverter;
}
public void init() {
if (!timeConfig.enabled())
return;
long period = timeConverter.calculateUpdatePeriod();
if (timeConfig.scale() * period != 72.0) {
// TODO log this properly
DebugLogger.info("Warning: RealTime scale is not optimal. Time will be out of sync.", 0);
}
new SyncTimeUpdateTask(timeConverter, timeConfig.dynamic()).runTaskTimer(plugin, 0, period);
if (timeConfig.dynamic())
new AsyncPlayerTimeTask(timeConverter).runTaskTimerAsynchronously(plugin, 0, 60); // 5 seconds
mapper.registerWorldLoadConsumer(world -> world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false));
mapper.registerWorldUnloadConsumer(world -> world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, true));
DebugLogger.info("time loaded, update every %d ticks", 1, period);
public boolean isDynamic() {
return dynamic;
}
}

View file

@ -1,29 +1,34 @@
package eu.m724.realweather.commands;
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.time.command;
import java.time.Duration;
import eu.m724.realweather.Configs;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.time.TimeConverter;
import eu.m724.realweather.time.TimeMaster;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.time.TimeConfig;
import eu.m724.wtapi.object.Coordinates;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder;
public class LocalTimeCommand implements CommandExecutor {
private final Mapper mapper = GlobalConstants.getMapper();
private final TimeConfig timeConfig = Configs.timeConfig();
private final TimeMaster timeMaster;
private final TimeConverter timeConverter;
private final CoordinatesLocationConverter coordinatesLocationConverter;
public LocalTimeCommand(TimeConverter timeConverter) {
this.timeConverter = timeConverter;
public LocalTimeCommand(TimeMaster timeMaster, CoordinatesLocationConverter coordinatesLocationConverter) {
this.coordinatesLocationConverter = coordinatesLocationConverter;
this.timeMaster = timeMaster;
this.timeConverter = timeMaster.getTimeConverter();
}
@Override
@ -46,8 +51,8 @@ public class LocalTimeCommand implements CommandExecutor {
sender.spigot().sendMessage(component);
if (timeConfig.dynamic() && player != null && player.hasPermission("realweather.dynamic")) {
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation());
if (timeMaster.isDynamic() && player != null && player.hasPermission("realweather.dynamic")) {
Coordinates coordinates = coordinatesLocationConverter.locationToCoordinates(player.getLocation());
long offsetTime = timeConverter.calculateZoneOffset(coordinates.longitude());
long offsetTimeTicks = timeConverter.millisToTicks(offsetTime);

View file

@ -1,11 +1,14 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.updater;
import eu.m724.jarupdater.environment.ConstantEnvironment;
import eu.m724.jarupdater.updater.Updater;
import eu.m724.jarupdater.verify.SignatureVerifier;
import eu.m724.jarupdater.verify.Verifier;
import eu.m724.realweather.Configs;
import org.bukkit.plugin.Plugin;
import eu.m724.jarupdater.download.Downloader;
import eu.m724.jarupdater.download.SimpleDownloader;
@ -13,25 +16,23 @@ import eu.m724.jarupdater.environment.Environment;
import eu.m724.jarupdater.live.GiteaMetadataDAO;
import eu.m724.jarupdater.live.MetadataDAO;
import eu.m724.jarupdater.live.MetadataFacade;
import eu.m724.realweather.RealWeatherPlugin;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
public class PluginUpdater extends Updater {
private final UpdaterConfig updaterConfig = Configs.updaterConfig();
final Plugin plugin;
PluginUpdater(Plugin plugin, Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
super(environment, metadataProvider, downloader, verifier);
this.plugin = plugin;
}
public static PluginUpdater build(Plugin plugin, File file) {
public static PluginUpdater build(File file, String channel) {
RealWeatherPlugin plugin = RealWeatherPlugin.getInstance();
Environment environment = new ConstantEnvironment(
plugin.getDescription().getVersion(),
Configs.updaterConfig().channel(),
channel,
file.toPath()
);
@ -46,13 +47,6 @@ public class PluginUpdater extends Updater {
throw new RuntimeException(e);
}
return new PluginUpdater(plugin, environment, metadataFacade, downloader, verifier);
}
public void init() {
if (!updaterConfig.alert()) return;
UpdateNotifier updateNotifier = new UpdateNotifier(this, (version) -> {});
updateNotifier.register();
return new PluginUpdater(environment, metadataFacade, downloader, verifier);
}
}

View file

@ -1,8 +1,14 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.updater;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import eu.m724.realweather.RealWeatherPlugin;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
@ -14,55 +20,51 @@ import eu.m724.jarupdater.object.Version;
import eu.m724.realweather.DebugLogger;
public class UpdateNotifier extends BukkitRunnable implements Listener { // TODO move this to jarupdater
private final Plugin plugin = RealWeatherPlugin.getInstance();
private final PluginUpdater updater;
private final Consumer<Version> updateConsumer;
private final Plugin plugin;
private Version latestVersion;
public UpdateNotifier(PluginUpdater updater, Consumer<Version> updateConsumer) {
public UpdateNotifier(PluginUpdater updater) {
this.updater = updater;
this.updateConsumer = updateConsumer;
this.plugin = updater.plugin;
}
public void register() {
this.runTaskTimerAsynchronously(updater.plugin, 0, 432000); // 6h
this.runTaskTimerAsynchronously(plugin, 0, 6 * 3600 * 20); // 6h
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void run() {
DebugLogger.info("update task running", 2);
DebugLogger.finer("Updater running");
try {
latestVersion = updater.getLatestVersion().join();
} catch (CompletionException e) {
Throwable ex = e.getCause();
DebugLogger.info("Error trying to contact update server: %s", 0, ex.getMessage());
if (DebugLogger.getDebugLevel() >= 1)
e.printStackTrace();
DebugLogger.warning("Error trying to contact update server:");
DebugLogger.warning(" %s", e.getCause().toString());
}
if (latestVersion == null) return;
DebugLogger.info("RealWeather is outdated. /rwadmin update", 0);
if (latestVersion == null) {
DebugLogger.fine("Plugin is up to date");
return;
}
DebugLogger.warning("RealWeather is outdated. /rwadmin update");
for (Player player : updater.plugin.getServer().getOnlinePlayers()) {
for (Player player : plugin.getServer().getOnlinePlayers()) {
if (player.hasPermission("realweather.update.notify")) {
player.sendMessage("RealWeather is outdated. /rwadmin update");
player.sendMessage(ChatColor.GOLD + "RealWeather is outdated. /rwadmin update");
}
}
updateConsumer.accept(latestVersion);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e) {
Player player = e.getPlayer();
if (latestVersion != null && player.hasPermission("realweather.update.notify")) {
player.sendMessage("RealWeather is outdated. /rwadmin update");
player.sendMessage(ChatColor.GOLD + "RealWeather is outdated. /rwadmin update");
}
}
}

View file

@ -1,20 +0,0 @@
package eu.m724.realweather.updater;
import org.bukkit.configuration.ConfigurationSection;
/**
*
* @param alert alert admins about updates
* @param channel update channel
*/
public record UpdaterConfig(
boolean alert, // this is different because I can't use notify in records sadly
String channel
) {
public static UpdaterConfig fromConfiguration(ConfigurationSection configuration) {
return new UpdaterConfig(
configuration.getBoolean("notify"),
configuration.getString("channel")
);
}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.updater.command;
import java.nio.file.NoSuchFileException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.updater.PluginUpdater;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class UpdateCommand {
private final PluginUpdater updater;
private boolean updatePending = false;
public UpdateCommand(PluginUpdater updater) {
this.updater = updater;
}
private void sendChangelogMessage(CommandSender sender, String changelogUrl) {
if (changelogUrl != null) {
if (sender instanceof Player) {
TextComponent textComponent = new TextComponent("Click here to open changelog");
textComponent.setUnderlined(true);
textComponent.setColor(ChatColor.AQUA);
textComponent.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText(changelogUrl)));
textComponent.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, changelogUrl));
sender.spigot().sendMessage(textComponent);
} else {
sender.sendMessage("Changelog: " + changelogUrl);
}
}
}
public void updateCommand(CommandSender sender, String[] args) {
sender.sendMessage(ChatColor.GRAY + "Please wait...");
sender.sendMessage(ChatColor.GRAY + "Channel: " + updater.getEnvironment().getChannel());
if (updatePending) {
sender.sendMessage(ChatColor.YELLOW + "" + ChatColor.BOLD + "(!) Server restart required");
}
String action = args.length > 1 ? args[1] : null; // remember this function is proxied
if (action == null) {
updater.getLatestVersion().thenAccept(metadata -> {
updater.getCurrentVersion().thenAccept(metadata2 -> {
sender.sendMessage(ChatColor.GOLD + "You're on RealWeather" + metadata2.getLabel() + " released " + formatDate(metadata2.getTimestamp()));
sendChangelogMessage(sender, metadata2.getChangelogUrl());
}).exceptionally(e -> {
sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details.");
DebugLogger.severe("Error retrieving information about current version:");
DebugLogger.severe(" " + e);
return null;
});
if (metadata != null) {
sender.sendMessage(ChatColor.YELLOW + "" + ChatColor.BOLD + "An update is available!");
sender.sendMessage(ChatColor.AQUA + "RealWeather " + metadata.getLabel() + ChatColor.GOLD + " released " + formatDate(metadata.getTimestamp()));
sendChangelogMessage(sender, metadata.getChangelogUrl());
sender.sendMessage(ChatColor.GOLD + "To download: /rwadmin update download");
} else {
sender.sendMessage(ChatColor.GRAY + "No new updates");
}
}).exceptionally(e -> {
sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details.");
DebugLogger.severe("Error checking for update:");
DebugLogger.severe(" " + e);
return null;
});
} else {
if (action.equals("download")) {
sender.sendMessage(ChatColor.GRAY + "Started download");
updater.downloadLatestVersion().thenAccept(file -> {
sender.sendMessage(ChatColor.GREEN + "Download finished, install with /rwadmin update install"); // TODO make this clickable
}).exceptionally(e -> {
sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details.");
DebugLogger.severe("Error downloading update:");
DebugLogger.severe(" " + e);
return null;
});
} else if (action.equals("install")) {
try {
updater.installLatestVersion().thenAccept(v -> {
sender.sendMessage(ChatColor.GREEN + "Installation completed, restart server to apply.");
updatePending = true;
}).exceptionally(e -> {
sender.sendMessage(ChatColor.RED + "An error has occurred, see console for details.");
DebugLogger.severe("Error installing update:");
DebugLogger.severe(" " + e);
return null;
});
} catch (NoSuchFileException e) {
sender.sendMessage(ChatColor.YELLOW + "Download the update first: /rwadmin update download");
}
}
}
}
private String formatDate(long timestamp) {
return LocalDate.ofEpochDay(timestamp / 86400).format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
}
}

View file

@ -1,123 +1,87 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.weather;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.api.weather.AsyncPlayerWeatherUpdateEvent;
import eu.m724.wtapi.provider.weather.WeatherQueryResult;
import org.bukkit.Server;
import org.bukkit.World;
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.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.wtapi.object.Coordinates;
import eu.m724.wtapi.object.Weather;
import eu.m724.wtapi.provider.weather.WeatherProvider;
public class DynamicWeatherRetriever extends BukkitRunnable implements Listener {
public class DynamicWeatherRetriever extends BukkitRunnable {
private final RealWeatherPlugin plugin;
private final WeatherProvider weatherProvider;
private final PlayerWeatherStore playerWeatherStore;
private final Mapper mapper = GlobalConstants.getMapper();
private final Plugin plugin = GlobalConstants.getPlugin();
private final Server server = plugin.getServer();
private final PlayerWeatherCache playerWeatherCache = GlobalConstants.getPlayerWeatherCache();
// when to next update all players
private long nextUpdate = 0;
// players that need update asap
private final Set<Player> neededUpdate = new HashSet<>();
private final CoordinatesLocationConverter coordinatesLocationConverter;
private final List<World> worlds;
public DynamicWeatherRetriever(WeatherProvider weatherProvider) {
public DynamicWeatherRetriever(RealWeatherPlugin plugin, WeatherProvider weatherProvider, PlayerWeatherStore playerWeatherStore) {
this.plugin = plugin;
this.weatherProvider = weatherProvider;
}
this.playerWeatherStore = playerWeatherStore;
private record CoordinatesResult(
Map<Coordinates, Player[]> coordinatesPlayersMap,
int coordinatesCount,
int playerCount
) {}
private CoordinatesResult makeCoordinates(Collection<? extends Player> players) {
Map<Coordinates, Player[]> map = new HashMap<>();
int coordinatesCount = 0;
int playerCount = 0;
long now = System.currentTimeMillis();
for (Player player : players) {
if (!player.hasPermission("realweather.dynamic")) continue;
if (!mapper.getWorlds().contains(player.getWorld())) continue;
Long lastUpdate = playerWeatherCache.getLastUpdate(player);
if (lastUpdate != null && now - lastUpdate < 10000) continue;
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation());
Coordinates closestCoordinates = null;
for (Coordinates potential : map.keySet()) {
//double distance = Math.sqrt(Math.pow(potential.latitude - coordinates.latitude, 2) + Math.pow(potential.longitude - potential.latitude, 2));
// TODO setup for "bundling" that is one request for close players
}
if (closestCoordinates != null) {
Player[] oldPlayerArray = map.get(coordinates);
Player[] newPlayerArray = Arrays.copyOf(oldPlayerArray, oldPlayerArray.length + 1);
newPlayerArray[oldPlayerArray.length] = player;
map.put(coordinates, newPlayerArray);
} else {
map.put(coordinates, new Player[] { player });
coordinatesCount++;
}
playerCount++;
}
return new CoordinatesResult(map, coordinatesCount, playerCount);
this.coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
this.worlds = plugin.getWorldList().getIncludedWorlds();
}
@Override
public void run() {
DebugLogger.info("Weather retrieval", 3);
long now = System.currentTimeMillis();
DebugLogger.finer("Updating weather");
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
CoordinatesResult coordinates;
if (now > nextUpdate) {
coordinates = makeCoordinates(server.getOnlinePlayers());
// calculate acceptable request rate based on weather provider quota and active players
float hourly = (float) weatherProvider.getQuota().getHourlyQuota() / coordinates.coordinatesCount();
nextUpdate = now + Math.max(60000, (long) (3600000 / hourly));
DebugLogger.info("Next update in %d", 3, nextUpdate);
} else { // immediate update for those that need it right now
if (neededUpdate.isEmpty()) return;
DebugLogger.info("Players in need of update: %d", 2, neededUpdate.size());
coordinates = makeCoordinates(neededUpdate);
neededUpdate.clear();
}
Coordinates[] coordinatesArray = coordinates.coordinatesPlayersMap().keySet().toArray(Coordinates[]::new);
float maxHourlyUpdates = (float) weatherProvider.getQuota().getHourlyQuota() / plugin.getServer().getOnlinePlayers().size();
long updateDelay = Math.max(60, (long) (3600 / maxHourlyUpdates));
if (coordinatesArray.length == 0) {
DebugLogger.info("nothing to update, dynamic retriever done", 3);
DebugLogger.finer("Update delay: %d seconds", updateDelay);
Player[] playersToUpdate = plugin.getServer().getOnlinePlayers().stream().filter(player -> {
LocalDateTime lastUpdate = playerWeatherStore.getLastUpdate(player);
if (!player.hasPermission("realweather.dynamic")) return false;
if (!worlds.contains(player.getWorld())) return false;
if (lastUpdate == null) return true;
DebugLogger.finer("Player %s's last update: %s", player.getName(), lastUpdate.toString());
if (Duration.between(lastUpdate, now).getSeconds() > updateDelay) return true;
// TODO also by distance
return false;
}).toArray(Player[]::new);
if (playersToUpdate.length == 0) {
DebugLogger.finer("Nobody needs updating");
return;
}
Coordinates[] coordinatesArray = Arrays.stream(playersToUpdate).map(player ->
coordinatesLocationConverter.locationToCoordinates(player.getLocation())
).toArray(Coordinates[]::new);
CompletableFuture<WeatherQueryResult> weathersFuture =
weatherProvider.getWeather(coordinatesArray);
WeatherQueryResult result = weathersFuture.join();
if (result.exception() != null) {
DebugLogger.info("An error occurred trying to retrieve weather data", 0);
if (DebugLogger.getDebugLevel() > 0) {
result.exception().printStackTrace();
}
DebugLogger.severe("An error has occurred retrieving weather data");
DebugLogger.warning(" " + result.exception());
return;
}
@ -126,22 +90,16 @@ public class DynamicWeatherRetriever extends BukkitRunnable implements Listener
for (int i=0; i<weathers.length; i++) {
Weather weather = weathers[i];
for (Player player : coordinates.coordinatesPlayersMap().get(coordinatesArray[i])) {
playerWeatherCache.put(player, weather, weather.timestamp().toEpochSecond(ZoneOffset.UTC));
Player player = playersToUpdate[i];
AsyncPlayerWeatherUpdateEvent event =
new AsyncPlayerWeatherUpdateEvent(player, weather);
playerWeatherStore.put(player, weather);
server.getPluginManager().callEvent(event);
}
AsyncPlayerWeatherUpdateEvent event =
new AsyncPlayerWeatherUpdateEvent(player, weather);
plugin.getServer().getPluginManager().callEvent(event);
}
DebugLogger.info("dynamic retriever done", 3);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
neededUpdate.add(player);
DebugLogger.fine("Done updating weather");
}
}

View file

@ -1,28 +0,0 @@
package eu.m724.realweather.weather;
import java.util.HashMap;
import org.bukkit.entity.Player;
import eu.m724.wtapi.object.Weather;
/**
* Stores player weathers and when they were last updated
*/
public class PlayerWeatherCache {
HashMap<Player, Weather> weathers = new HashMap<>();
HashMap<Player, Long> lastUpdates = new HashMap<>();
void put(Player player, Weather weather, Long lastUpdate) {
weathers.put(player, weather);
lastUpdates.put(player, lastUpdate);
}
public Weather getWeather(Player player) {
return weathers.get(player);
}
public Long getLastUpdate(Player player) {
return lastUpdates.get(player);
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.weather;
import java.time.LocalDateTime;
import java.util.HashMap;
import org.bukkit.entity.Player;
import eu.m724.wtapi.object.Weather;
public class PlayerWeatherStore {
private final HashMap<Player, Weather> weathers = new HashMap<>();
void put(Player player, Weather weather) {
weathers.put(player, weather);
}
public Weather getWeather(Player player) {
return weathers.get(player);
}
public LocalDateTime getLastUpdate(Player player) {
Weather weather = weathers.get(player);
return weather != null ? weather.timestamp() : null;
}
}

View file

@ -1,37 +1,42 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.weather;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.api.weather.AsyncGlobalWeatherUpdateEvent;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.wtapi.object.Coordinates;
import eu.m724.wtapi.object.Weather;
import eu.m724.wtapi.provider.weather.WeatherProvider;
import eu.m724.wtapi.provider.weather.WeatherQueryResult;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
public class StaticWeatherRetriever extends BukkitRunnable {
private final Plugin plugin = GlobalConstants.getPlugin();
private final Mapper mapper = GlobalConstants.getMapper();
private final RealWeatherPlugin plugin;
private final WeatherProvider weatherProvider;
private final CoordinatesLocationConverter coordinatesLocationConverter;
public StaticWeatherRetriever(WeatherProvider weatherProvider) {
public StaticWeatherRetriever(RealWeatherPlugin plugin, WeatherProvider weatherProvider) {
this.plugin = plugin;
this.weatherProvider = weatherProvider;
this.coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
}
@Override
public void run() {
Coordinates point = mapper.getPoint();
DebugLogger.finer("Updating weather");
Coordinates point = coordinatesLocationConverter.getStaticPoint();
WeatherQueryResult result = weatherProvider.getWeather(point).join();
if (result.exception() != null) {
DebugLogger.info("An error occurred trying to retrieve weather data", 0);
if (DebugLogger.getDebugLevel() > 0)
result.exception().printStackTrace();
DebugLogger.severe("An error has occurred retrieving weather data");
DebugLogger.warning(" " + result.exception());
return;
}
@ -43,6 +48,6 @@ public class StaticWeatherRetriever extends BukkitRunnable {
plugin.getServer().getPluginManager().callEvent(event);
DebugLogger.info("static weather retriever is done", 3);
DebugLogger.fine("Done updating weather");
}
}

View file

@ -1,10 +1,14 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.weather;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.api.weather.AsyncGlobalWeatherUpdateEvent;
import eu.m724.realweather.api.weather.AsyncPlayerWeatherUpdateEvent;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.map.WorldList;
import eu.m724.wtapi.object.Weather;
import org.bukkit.WeatherType;
import org.bukkit.entity.Player;
@ -12,26 +16,35 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
// TODO make weather more comprehensive
// TODO make weather more intricate
public class WeatherChanger implements Listener {
private final Mapper mapper = GlobalConstants.getMapper();
private final WorldList worldList;
public WeatherChanger(WorldList worldList) {
this.worldList = worldList;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onGlobalWeatherUpdate(AsyncGlobalWeatherUpdateEvent event) {
if (event.isCancelled()) return;
Weather weather = event.getWeather();
DebugLogger.info("Changing weather static", 3);
mapper.getWorlds().forEach(w -> {
DebugLogger.info("Changing weather static in world %s", 2, w.getName());
DebugLogger.finer("Changing weather static");
worldList.getIncludedWorlds().forEach(w -> {
if (weather.thundering()) {
DebugLogger.finer("Changing weather static in world %s to thunder", w.getName());
w.setClearWeatherDuration(0);
w.setWeatherDuration(120000);
w.setThunderDuration(120000);
} else if (weather.raining() || weather.snowing()) {
DebugLogger.finer("Changing weather static in world %s to rain", w.getName());
w.setClearWeatherDuration(0);
w.setWeatherDuration(120000);
w.setThunderDuration(0);
} else {
DebugLogger.finer("Changing weather static in world %s to clear", w.getName());
w.setClearWeatherDuration(120000);
w.setWeatherDuration(0);
w.setThunderDuration(0);
@ -41,14 +54,16 @@ public class WeatherChanger implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerWeatherUpdate(AsyncPlayerWeatherUpdateEvent event) {
if (event.isCancelled()) return;
Player player = event.getPlayer();
Weather weather = event.getWeather();
DebugLogger.info("Changing weather for player %s", 2, player.getName());
if (weather.thundering() || weather.snowing() || weather.raining()) {
DebugLogger.finer("Changing weather for player %s to downfall", player.getName());
player.setPlayerWeather(WeatherType.DOWNFALL);
} else {
DebugLogger.finer("Changing weather for player %s to clear", player.getName());
player.setPlayerWeather(WeatherType.CLEAR);
}
}

View file

@ -1,3 +1,8 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.weather;
import org.bukkit.configuration.ConfigurationSection;
@ -5,15 +10,15 @@ import org.bukkit.configuration.ConfigurationSection;
/**
* Configuration of the weather module
*
* @param enabled Is weather module enabled
* @param provider The provider name, may or may not exist, if it doesn't, an error is thrown later
* @param apiKey API key for the provider
* @param enabled Whether the weather module is enabled
* @param provider The provider name
* @param apiKey API key for the provider, null if not necessary
* @param dynamic dynamic mode, weather is per player or global
*/
public record WeatherConfig(
boolean enabled,
String provider,
String apiKey, // TODO don't expose that, I mean it's only used in one place in init
String apiKey, // TODO don't expose that, it's only used in one place in init
boolean dynamic
) {
public static WeatherConfig fromConfiguration(ConfigurationSection configuration) {

View file

@ -1,49 +1,74 @@
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
package eu.m724.realweather.weather;
import eu.m724.realweather.Configs;
import eu.m724.wtapi.provider.exception.NoSuchProviderException;
import org.bukkit.GameRule;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.wtapi.provider.exception.NoSuchProviderException;
import eu.m724.wtapi.provider.Providers;
import eu.m724.wtapi.provider.exception.ProviderException;
import eu.m724.wtapi.provider.weather.WeatherProvider;
public class WeatherMaster {
private final WeatherConfig config = Configs.weatherConfig();
private final Mapper mapper = GlobalConstants.getMapper();
/**
private final RealWeatherPlugin plugin;
private final boolean dynamic;
private final String providerName;
private final String apiKey;
private final PlayerWeatherStore playerWeatherStore = new PlayerWeatherStore();
public WeatherMaster(RealWeatherPlugin plugin, WeatherConfig weatherConfig) {
this.plugin = plugin;
this.dynamic = weatherConfig.dynamic();
this.providerName = weatherConfig.provider();
this.apiKey = weatherConfig.apiKey();
}
/**
* initializes, tests and starts
* @throws ProviderException if provider initialization failed
* @throws NoSuchProviderException config issue
*/
public void init(Plugin plugin) throws ProviderException, NoSuchProviderException {
if (!config.enabled()) {
DebugLogger.info("weather module is disabled", 1);
return;
}
WeatherProvider provider = Providers.getWeatherProvider(config.provider(), config.apiKey());
public void init() throws ProviderException, NoSuchProviderException {
WeatherProvider provider = Providers.getWeatherProvider(providerName, apiKey);
provider.init();
if (config.dynamic()) {
DynamicWeatherRetriever retriever = new DynamicWeatherRetriever(provider);
if (dynamic) {
DebugLogger.finer("Weather is dynamic");
DynamicWeatherRetriever retriever = new DynamicWeatherRetriever(plugin, provider, playerWeatherStore);
retriever.runTaskTimerAsynchronously(plugin,0, 200);
plugin.getServer().getPluginManager().registerEvents(retriever, plugin);
} else {
StaticWeatherRetriever retriever = new StaticWeatherRetriever(provider);
DebugLogger.finer("Weather is static");
StaticWeatherRetriever retriever = new StaticWeatherRetriever(plugin, provider);
retriever.runTaskTimerAsynchronously(plugin,0, 60000);
}
plugin.getServer().getPluginManager().registerEvents(new WeatherChanger(), plugin);
mapper.registerWorldLoadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, false));
mapper.registerWorldUnloadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, true));
DebugLogger.info("weather loaded", 1);
plugin.getServer().getPluginManager().registerEvents(new WeatherChanger(plugin.getWorldList()), plugin);
DebugLogger.finer("Done initializing");
// TODO replace that
// coordinatesLocationConverter.registerWorldLoadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, false));
// coordinatesLocationConverter.registerWorldUnloadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, true));
}
public boolean isDynamic() {
return dynamic;
}
// TODO should this be exposed?
public String getProviderName() {
return providerName;
}
public PlayerWeatherStore getPlayerWeatherStore() {
return playerWeatherStore;
}
}

View file

@ -1,35 +1,42 @@
package eu.m724.realweather.commands;
/*
* Copyright (c) 2025 RealWeather Authors
* RealWeather is licensed under the GNU General Public License. See the LICENSE.md file in the project root for the full license text.
*/
import java.time.Instant;
import java.time.ZoneOffset;
package eu.m724.realweather.weather.command;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import eu.m724.realweather.weather.PlayerWeatherStore;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.weather.PlayerWeatherCache;
import eu.m724.wtapi.object.Weather;
public class LocalWeatherCommand implements CommandExecutor {
private final PlayerWeatherCache playerWeatherCache = GlobalConstants.getPlayerWeatherCache();
private final PlayerWeatherStore playerWeatherStore;
@Override
public LocalWeatherCommand(PlayerWeatherStore playerWeatherStore) {
this.playerWeatherStore = playerWeatherStore;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("You must be a player to use this command");
return true;
}
Weather weather = playerWeatherCache.getWeather(player);
Weather weather = playerWeatherStore.getWeather(player);
if (weather != null) {
long lastUpdate = playerWeatherCache.getLastUpdate(player);
LocalDateTime lastUpdate = playerWeatherStore.getLastUpdate(player);
colorize(sender, "&6Weather for: &b%f&7, &b%f &7(lat, lon)\n", weather.coordinates().latitude(), weather.coordinates().longitude());
@ -48,10 +55,9 @@ public class LocalWeatherCommand implements CommandExecutor {
colorize(sender, "&6Cloud cover (cloudiness): &b%.0f&7%%", weather.cloudCoverPercentage() * 100);
colorize(sender, "&6Relative humidity: &b%.0f&7%%", weather.relativeHumidityPercentage() * 100);
colorize(sender, "&6Last update: &b%s UTC\n", formatTime(lastUpdate));
colorize(sender, "&6Last update: &b%s UTC\n", lastUpdate.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
} else {
colorize(sender, "&6No weather for you, try again in a second");
colorize(sender, "&6No weather for you yet, try again in a few seconds");
}
return true;
@ -60,9 +66,4 @@ public class LocalWeatherCommand implements CommandExecutor {
private void colorize(CommandSender sender, String text, Object... format) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', text.formatted(format)));
}
private String formatTime(long timestamp) {
return DateTimeFormatter.ofPattern("HH:mm:ss").format(Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC));
}
}

View file

@ -2,22 +2,11 @@
### GENERAL SETTINGS ###
############################
# Master switch
enabled: false
# Not much to be found here! Explore the other files.
updater:
# Notify players and console about plugin updates
# This also controls automatic checking
# You can still update with /rwadmin update
# Relevant permission node: realweather.update.notify
notify: true
# stable for stable releases
# testing for latest builds (untested hence the name)
# As there's no release yet, stable will just error
channel: testing
# 0 - no debug
# 1 - debug loading modules
# 2 - also debug processing conditions
# 3 - also log tasks running, this will spam
debug: 0
notify: true

View file

@ -2,21 +2,20 @@
### MAP SETTINGS ###
############################
# true if the list below is a blacklist, false otherwise
worldBlacklist: false
worlds:
- world
isBlacklist: false
dimensions:
# Blocks per 1 deg, can't be decimal
# latitude = -Z, longitude = X (to match with F3)
# The default (111000) assumes 1 block = 1 meter
latitude: 111000 # -9990000 to 9990000 Z
longitude: 111000 # -19980000 to 19980000 X
# Blocks per 1 deg, must be integer
# latitude+ = Z-, longitude+ = X+ (to match with F3)
# The default (111,000) assumes 1 block = 1 meter
latitude: 111_000 # -9,990,000 to 9,990,000 Z. Wraps beyond that
longitude: 111_000 # -19,980,000 to 19,980,000 X
# To make it span world border to world border:
# latitude: 333333
# longitude: 166666
# latitude: 333_333
# longitude: 166_666
# For `static` mode
point:

View file

@ -6,10 +6,7 @@ enabled: false
# Currently only blitzortung
provider: blitzortung
# No API key needed
# How often should we poll for updates and spawn lightning
# This is a synchronous task
# Exaggerating, if you put it too low you'll have lag,
# But if you put it too high you'll have lag spikes and weird lightning
# In ticks, default 100 is 5 seconds so reduce if lightning seems weird, in my testing even 5 ticks is fine
refreshPeriod: 100
# Lightning is DYNAMIC.
# Settings are in map.yml

View file

@ -2,20 +2,19 @@
### TIME SETTINGS ###
############################
# Warning: this removes sleep
# No, it's not a bug. It would de-synchronize, and can you skip time IRL?
# Can you believe that I actually used to consider this a bug?
# Warning: this removes sleep. No, not a bug, as you can't skip time IRL.
enabled: false
# How this plugin affects your world:
# How time is applied:
# - static (false): time is the same across the world
# - dynamic (true): static + local time for each player, however it's only cosmetic so it will not match mobs spawning etc
# - dynamic (true): local time based on player's location. However, it's only cosmetic, so it will not match mobs spawning, etc.
# Settings for both are in map.yml
dynamic: true
# x in game day cycles in 1 irl day cycle
# Real days per in-game day
# 2.0 - time goes 2x SLOWER
# 0.5 - time goes 2x FASTER
# If modified, time will no longer be in sync with real life
# If modified, time will no longer be in sync with real life. Unreal time.
# The division 72 / scale should equal an integer!
scale: 1.0

View file

@ -13,8 +13,8 @@ enabled: false
provider: openmeteo
# No API key needed
# How this plugin affects your world:
# How weather is applied:
# - static (false): weather is the same across the world
# - dynamic (true): weather is per player, however it's only cosmetic so it will not match mobs spawning etc
# settings for both are in map.yml
# - dynamic (true): weather is based on player's location. However, it's only cosmetic, so it will not match mobs spawning, etc.
# Settings for both are in map.yml
dynamic: true