Compare commits

..

14 commits

Author SHA1 Message Date
39e1186911
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ deploy (push) Successful in 1m28s
2025-05-26 19:40:18 +02:00
f35b931f77
[maven-release-plugin] prepare release realweather-1.0.0-alpha-7
All checks were successful
/ deploy (push) Successful in 1m25s
2025-05-26 19:40:16 +02:00
0a4798656f
Fix warning
Some checks failed
/ deploy (push) Has been cancelled
2025-05-26 19:39:37 +02:00
c51f06d2b5
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ deploy (push) Successful in 1m36s
2025-05-26 06:56:01 +02:00
1895e90610
[maven-release-plugin] prepare release realweather-1.0.0-alpha-6
All checks were successful
/ deploy (push) Successful in 1m30s
2025-05-26 06:55:58 +02:00
8d1e5d3a3b
Some refactoring
Some checks failed
/ deploy (push) Has been cancelled
2025-05-26 06:55:31 +02:00
0e8c6e5460
Remove unused exception
All checks were successful
/ deploy (push) Successful in 1m34s
2025-05-25 08:56:45 +02:00
a33022380a
Split into two events 2025-05-25 08:46:59 +02:00
505d8df3d6
Downgrade to 1.16.5
All checks were successful
/ deploy (push) Successful in 1m25s
2025-05-24 14:58:09 +02:00
d55939c116
Add mStats 2025-05-24 14:56:32 +02:00
7927d8ba3e
Update wtapi
All checks were successful
/ deploy (push) Successful in 2m0s
No major refactoring
2025-05-24 14:48:53 +02:00
Minecon724
1920d8dcc2
Make scale a double
All checks were successful
/ deploy (push) Successful in 38s
2025-01-18 11:43:29 +01:00
96454e22ec Fix /localweather
All checks were successful
/ deploy (push) Successful in 36s
2025-01-15 07:10:20 +01:00
Minecon724
f61783c1a5
[maven-release-plugin] prepare for next development iteration
All checks were successful
/ deploy (push) Successful in 1m20s
2024-11-06 17:29:01 +01:00
56 changed files with 1470 additions and 1206 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>

2
.idea/modules.xml generated
View file

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/realweather.iml" filepath="$PROJECT_DIR$/realweather.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/realweather.iml" filepath="$PROJECT_DIR$/.idea/realweather.iml" />
</modules> </modules>
</component> </component>
</project> </project>

13
.idea/realweather.iml generated Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="minecraft" name="Minecraft">
<configuration>
<autoDetectTypes>
<platformType>SPIGOT</platformType>
</autoDetectTypes>
<projectReimportVersion>1</projectReimportVersion>
</configuration>
</facet>
</component>
</module>

View file

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

View file

@ -1,6 +1,6 @@
# realweather # realweather
For MC 1.19.4+ and Java 21+ For MC 1.16.5+ and Java 21+
### Building ### Building
To compile, clone this repo and `mvn clean package`. \ To compile, clone this repo and `mvn clean package`. \

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

13
pom.xml
View file

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>eu.m724</groupId> <groupId>eu.m724</groupId>
<artifactId>realweather</artifactId> <artifactId>realweather</artifactId>
<version>1.0.0-alpha-5</version> <version>1.0.0-alpha-8-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
@ -29,7 +29,7 @@
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version> <version>1.16.5-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
<!-- Fix warning about vulnerabilities of things we don't use --> <!-- Fix warning about vulnerabilities of things we don't use -->
<exclusions> <exclusions>
@ -46,13 +46,18 @@
<dependency> <dependency>
<groupId>eu.m724</groupId> <groupId>eu.m724</groupId>
<artifactId>wtapi</artifactId> <artifactId>wtapi</artifactId>
<version>0.8.3</version> <version>0.9.3</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>eu.m724</groupId> <groupId>eu.m724</groupId>
<artifactId>jarupdater</artifactId> <artifactId>jarupdater</artifactId>
<version>0.1.8</version> <version>0.1.8</version>
</dependency> </dependency>
<dependency>
<groupId>eu.m724</groupId>
<artifactId>mstats-spigot</artifactId>
<version>0.1.2</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -145,6 +150,6 @@
<scm> <scm>
<developerConnection>scm:git:git@git.m724.eu:Minecon724/realweather.git</developerConnection> <developerConnection>scm:git:git@git.m724.eu:Minecon724/realweather.git</developerConnection>
<tag>realweather-1.0.0-alpha-5</tag> <tag>HEAD</tag>
</scm> </scm>
</project> </project>

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; 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.thunder.ThunderConfig;
import eu.m724.realweather.time.TimeConfig; import eu.m724.realweather.time.TimeConfig;
import eu.m724.realweather.updater.UpdaterConfig; import eu.m724.realweather.weather.WeatherChanger;
import eu.m724.realweather.weather.WeatherConfig;
// TODO replaces GlobalConstants for configs public record Configs(
public class Configs { WeatherChanger weatherConfig,
static WeatherConfig weatherConfig; TimeConfig timeConfig,
static TimeConfig timeConfig; ThunderConfig thunderConfig,
static ThunderConfig thunderConfig; MapConfig mapConfig
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; }
} }

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; package eu.m724.realweather;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DebugLogger { public class DebugLogger {
static Logger baseLogger; private DebugLogger() {}
static int debugLevel; static Logger logger;
public static int getDebugLevel() { public static void info(String message, Object... format) {
return debugLevel; log(Level.INFO, message, format);
} }
public static void warning(String message, Object... format) {
public static void info(String message, int minDebugLevel, Object... format) { log(Level.WARNING, message, format);
if (debugLevel >= minDebugLevel)
baseLogger.info(message.formatted(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; package eu.m724.realweather;
import java.io.File; import java.io.File;
@ -5,149 +10,208 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.logging.Logger; import java.util.logging.Level;
import eu.m724.mstats.MStatsPlugin;
import eu.m724.realweather.map.WorldList;
import eu.m724.realweather.updater.UpdateNotifier;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import eu.m724.realweather.commands.AdminCommand; import eu.m724.realweather.commands.AdminCommand;
import eu.m724.realweather.commands.GeoCommand; import eu.m724.realweather.map.command.GeoCommand;
import eu.m724.realweather.commands.LocalTimeCommand; import eu.m724.realweather.time.command.LocalTimeCommand;
import eu.m724.realweather.commands.LocalWeatherCommand; import eu.m724.realweather.weather.command.LocalWeatherCommand;
import eu.m724.realweather.exception.UserError; import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.mapper.Mapper; import eu.m724.realweather.map.MapConfig;
import eu.m724.realweather.mapper.MapperConfig;
import eu.m724.realweather.thunder.ThunderConfig; import eu.m724.realweather.thunder.ThunderConfig;
import eu.m724.realweather.thunder.ThunderMaster; import eu.m724.realweather.thunder.ThunderMaster;
import eu.m724.realweather.time.TimeConfig; import eu.m724.realweather.time.TimeConfig;
import eu.m724.realweather.time.TimeMaster; import eu.m724.realweather.time.TimeMaster;
import eu.m724.realweather.updater.PluginUpdater; 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.WeatherConfig;
import eu.m724.realweather.weather.WeatherMaster; import eu.m724.realweather.weather.WeatherMaster;
import eu.m724.wtapi.provider.exception.NoSuchProviderException;
import eu.m724.wtapi.provider.exception.ProviderException;
// TODO unmess this too // TODO unmess this too
public class RealWeatherPlugin extends JavaPlugin { public class RealWeatherPlugin extends MStatsPlugin {
private Configs configs;
private WorldList worldList;
private WeatherMaster weatherMaster; private WeatherMaster weatherMaster;
private ThunderMaster thunderMaster;
private TimeMaster timeMaster; private TimeMaster timeMaster;
private PluginUpdater updater; private ThunderMaster thunderMaster;
private Logger logger; private static RealWeatherPlugin INSTANCE;
private CoordinatesLocationConverter coordinatesLocationConverter;
@Override @Override
public void onEnable() { public void onEnable() {
logger = getLogger(); long start = System.nanoTime();
INSTANCE = this;
File dataFolder = getDataFolder(); File dataFolder = getDataFolder();
File modulesFolder = new File("modules"); File modulesFolder = new File("modules");
modulesFolder.mkdir(); modulesFolder.mkdir();
YamlConfiguration configuration,
mapConfiguration, weatherConfiguration,
thunderConfiguration, timeConfiguration;
DebugLogger.info("loading configurations", 1);
boolean firstRun = !new File(dataFolder, "config.yml").exists(); boolean firstRun = !new File(dataFolder, "config.yml").exists();
YamlConfiguration configuration;
try { try {
configuration = getConfig("config.yml"); 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) { } catch (IOException e) {
logger.severe("Failed to load config!"); DebugLogger.severe("Failed to load configuration:");
throw new RuntimeException(e); DebugLogger.severe(" " + e);
getServer().getPluginManager().disablePlugin(this);
return;
} }
DebugLogger.logger = getLogger();
getLogger().setLevel(configuration.getBoolean("debug") ? Level.FINEST : Level.INFO);
if (firstRun) { if (firstRun) {
logger.warning("This is your first time running this plugin."); DebugLogger.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.)"); DebugLogger.warning("Please *shut down* the server, review the config files (enable modules, enter API keys, etc.)");
logger.warning("Don't forget to enable the plugin in config.yml"); DebugLogger.warning("Don't forget to enable the plugin in config.yml");
getServer().getPluginManager().disablePlugin(this); getServer().getPluginManager().disablePlugin(this);
return; return;
} }
DebugLogger.baseLogger = logger; if (configuration.getBoolean("disabled")) {
DebugLogger.debugLevel = configuration.getInt("debug"); DebugLogger.warning("Plugin disabled per config. Enable it in config.yml");
if (!configuration.getBoolean("enabled")) {
logger.warning("Plugin disabled by administrator. Enable it in config.yml");
getServer().getPluginManager().disablePlugin(this); getServer().getPluginManager().disablePlugin(this);
return; 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 { try {
DebugLogger.info("loading weather", 1); loadMapModules();
Configs.weatherConfig = WeatherConfig.fromConfiguration(weatherConfiguration); } catch (Exception e) {
weatherMaster = new WeatherMaster(); DebugLogger.severe("Failed to load the Map module:");
weatherMaster.init(this); DebugLogger.severe(" " + e);
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();
Configs.updaterConfig = UpdaterConfig.fromConfiguration(configuration.getConfigurationSection("updater"));
updater = PluginUpdater.build(this, this.getFile());
//updater.init();
} catch (UserError | NoSuchProviderException e) {
logger.severe("There are errors 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); getServer().getPluginManager().disablePlugin(this);
return; return;
} }
getCommand("rwadmin").setExecutor(new AdminCommand(updater, thunderMaster)); try {
getCommand("geo").setExecutor(new GeoCommand()); loadWeatherModule();
} catch (Exception e) {
DebugLogger.severe("Failed to load the Weather module:");
DebugLogger.severe(" " + e);
if (Configs.timeConfig.enabled()) getServer().getPluginManager().disablePlugin(this);
getCommand("localtime").setExecutor(new LocalTimeCommand(timeMaster.getTimeConverter())); return;
if (Configs.weatherConfig.enabled()) {
getCommand("localweather").setExecutor(new LocalWeatherCommand());
} }
/*Metrics metrics = new Metrics(this, 15020); try {
metrics.addCustomChart(new SimplePie("weather_provider", () -> loadThunderModule();
GlobalConstants.weatherConfig.enabled ? GlobalConstants.weatherConfig.provider : "off" } catch (Exception e) {
)); DebugLogger.severe("Failed to load the Thunder module:");
metrics.addCustomChart(new SimplePie("thunder_provider", () -> DebugLogger.severe(" " + e);
GlobalConstants.thunderConfig.enabled ? GlobalConstants.thunderConfig.provider : "off"
));
metrics.addCustomChart(new SimplePie("real_time", () ->
GlobalConstants.timeConfig.enabled() ? "on" : "off"
));*/
DebugLogger.info("ended loading", 1); 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.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 { public YamlConfiguration getConfig(String configFilePath) throws IOException {
@ -155,15 +219,50 @@ public class RealWeatherPlugin extends JavaPlugin {
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
if (!configFile.exists()) { if (!configFile.exists()) {
final InputStream defConfigStream = getResource(configFilePath); try (InputStream defConfigStream = getResource(configFilePath)) {
if (defConfigStream == null)
if (defConfigStream == null) return null;
return null;
config = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, StandardCharsets.UTF_8));
config = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, StandardCharsets.UTF_8)); }
config.save(configFile); config.save(configFile);
} }
return config; 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

@ -0,0 +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.api.weather;
import eu.m724.wtapi.object.Weather;
/**
* Called when weather is <em>updated</em> for the <strong>server</strong>
* <br>
* This is only used on <em>static</em> mode. For the dynamic mode counterpart, see {@link AsyncPlayerWeatherUpdateEvent}
*/
public class AsyncGlobalWeatherUpdateEvent extends AsyncWeatherUpdateEvent {
public AsyncGlobalWeatherUpdateEvent(Weather weather) {
super(weather);
}
}

View file

@ -0,0 +1,33 @@
/*
* 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;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Called when weather is <em>updated</em> for a player
* <br>
* This is only used on <em>dynamic</em> mode. For the static mode counterpart, see {@link AsyncGlobalWeatherUpdateEvent}
*/
public class AsyncPlayerWeatherUpdateEvent extends AsyncWeatherUpdateEvent {
private static final HandlerList HANDLERS = new HandlerList();
private final Player player;
public AsyncPlayerWeatherUpdateEvent(Player player, Weather weather) {
super(weather);
this.player = player;
}
/**
* @return The player to update weather for
*/
public Player getPlayer() {
return player;
}
}

View file

@ -1,65 +1,55 @@
/*
* 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; package eu.m724.realweather.api.weather;
import org.bukkit.entity.Player; import eu.m724.wtapi.object.Weather;
import org.bukkit.event.Cancellable; import org.bukkit.event.Cancellable;
import org.bukkit.event.Event; import org.bukkit.event.Event;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import eu.m724.wtapi.object.Weather; class AsyncWeatherUpdateEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
/** private final Weather weather;
* Fired when a weather state is retrieved<br>
* It doesn't mean the weather has changed, just that we retrieved the state<br>
*/
public class AsyncWeatherUpdateEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final Player player;
private final Weather weather;
private boolean cancelled; private boolean cancelled;
public AsyncWeatherUpdateEvent(Player player, Weather weather) { public AsyncWeatherUpdateEvent(Weather weather) {
super(true); super(true);
this.player = player; this.weather = weather;
this.weather = weather; }
}
/** /**
* @return a player that the weather is for, null if worldwide (static mode) * @return The new weather
*/ */
public Player getPlayer() { public Weather getWeather() {
return player; return weather;
} }
/** public static HandlerList getHandlerList() {
* @return the weather state that was just changed return HANDLERS;
*/ }
public Weather getWeather() {
return weather;
}
public static HandlerList getHandlerList() { @Override
return HANDLERS; public HandlerList getHandlers() {
} return HANDLERS;
}
@Override @Override
public HandlerList getHandlers() { public boolean isCancelled() {
return HANDLERS; return cancelled;
} }
@Override /**
public boolean isCancelled() { * Cancel the weather change
return cancelled; *
} * @param cancelled Whether to cancel
*/
/** @Override
* Cancel weather change<br> public void setCancelled(boolean cancelled) {
* It will only cancel changing the actual weather by the plugin, not retrieving and caching it this.cancelled = cancelled;
* @param cancelled to cancel or not }
*/
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
} }

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; 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.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; 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.thunder.ThunderMaster;
import eu.m724.realweather.time.TimeConfig;
import eu.m724.realweather.updater.PluginUpdater; import eu.m724.realweather.updater.PluginUpdater;
import eu.m724.realweather.weather.WeatherConfig;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
// TODO unmess this all
public class AdminCommand implements CommandExecutor { public class AdminCommand implements CommandExecutor {
private final RealWeatherPlugin plugin;
private final UpdateCommand updateCommand; private final UpdateCommand updateCommand;
private final Plugin plugin = GlobalConstants.getPlugin();
private final WeatherMaster weatherMaster;
private final WeatherConfig weatherConfig = Configs.weatherConfig(); private final TimeMaster timeMaster;
private final TimeConfig timeConfig = Configs.timeConfig();
private final ThunderConfig thunderConfig = Configs.thunderConfig();
private final MapperConfig mapperConfig = Configs.mapperConfig();
private final ThunderMaster thunderMaster; 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.updateCommand = new UpdateCommand(updater);
this.thunderMaster = thunderMaster; this.weatherMaster = plugin.getWeatherMaster();
this.timeMaster = plugin.getTimeMaster();
this.thunderMaster = plugin.getThunderMaster();
this.coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
} }
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0 && args[0].equals("update")) { 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, "\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 (weatherMaster != null) {
colorize(sender, "&6Weather: %s", weatherMaster.isDynamic() ? "&aYes, dynamic" : "&aYes, static");
if (weatherConfig.enabled()) { colorize(sender, " &6Provider: &b%s", weatherMaster.getProviderName());
colorize(sender, " &6Provider: &b%s", weatherConfig.provider());
colorize(sender, " &6/localweather to see current weather"); 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"); sender.sendMessage("");
if (timeConfig.enabled()) { if (timeMaster != null) {
colorize(sender, " &6Scale: &b%s&7x", timeConfig.scale()); 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"); colorize(sender, " &6/localtime to see current time");
} else {
colorize(sender, "&6Time: &cDisabled");
} }
colorize(sender, "\n&6Thunder: %s", thunderConfig.enabled() ? "&aYes, dynamic" : "&cDisabled"); sender.sendMessage("");
if (thunderConfig.enabled()) { if (thunderMaster != null) {
colorize(sender, " &6Provider: &b%s", thunderConfig.provider()); colorize(sender, "&6Thunder: &aYes, dynamic");
colorize(sender, " &6Refresh period: &b%d &7ticks", thunderConfig.refreshPeriod()); colorize(sender, " &6Provider: &b%s", thunderMaster.getProviderName());
colorize(sender, " &6API latency: &b%d&7ms", thunderMaster.getLatency()); colorize(sender, " &6API latency: &b%d&7ms", thunderMaster.getLatency());
} else {
colorize(sender, "&6Thunder: &cDisabled");
} }
return true; return true;

View file

@ -1,106 +0,0 @@
package eu.m724.realweather.commands;
import net.md_5.bungee.api.chat.*;
import net.md_5.bungee.api.chat.hover.content.Content;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.Location;
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.weather.PlayerWeatherCache;
import eu.m724.wtapi.object.Coordinates;
import eu.m724.wtapi.object.Weather;
import net.md_5.bungee.api.ChatColor;
public class GeoCommand implements CommandExecutor {
private final PlayerWeatherCache playerWeatherCache = GlobalConstants.getPlayerWeatherCache();
private final Mapper mapper = GlobalConstants.getMapper();
@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);
colorize(player, "");
colorize(player, "&6Geolocation: &b%f&7, %b%f &7(lat, lon)", coordinates.latitude, coordinates.longitude);
colorize(player, "&7Position: &3%f&8, %3%f &8(x, z)", location.getX(), location.getZ());
colorize(player, "&7City: &3%s", address);
colorize(player, "");
}
} else if (args.length >= 3) {
colorize(sender, "&cInvalid arguments, &7make sure it's &a\"/geo lat,lon\" &7or &a\"/geo x z\" &7or just &a\"/geo\"");
} else if (args.length == 2) {
double x, z;
try {
x = Double.parseDouble(args[0]);
z = Double.parseDouble(args[1]);
} catch (NumberFormatException e) {
colorize(sender, "&cInvalid arguments, &7make sure it's &a\"/geo lat,lon\" &7or &a\"/geo x z\" &7or just &a\"/geo\"");
return true;
}
Location location = new Location(null, x, 0, z);
Coordinates coordinates = mapper.locationToCoordinates(location);
colorize(sender, "");
colorize(sender, "&6Position: &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 with a space");
colorize(sender, "");
return true;
} else {
double latitude, longitude;
try {
String[] split = args[0].split(",");
latitude = Double.parseDouble(split[0]);
longitude = Double.parseDouble(split[1]);
} catch (NumberFormatException e) {
colorize(sender, "&cInvalid arguments, &7make sure it's &a\"/geo lat,lon\" &7or &a\"/geo x z\" &7or just &a\"/geo\"");
return true;
}
Coordinates coordinates = new Coordinates(latitude, longitude);
Location location = mapper.coordinatesToLocation(null, coordinates);
colorize(sender, "");
colorize(sender, "&6Position: &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, "");
}
return true;
}
private void colorize(CommandSender sender, String text, Object... format) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', text.formatted(format)));
}
private String formatAddress(Weather weather) {
if (weather == null) return "Not retrieved yet";
Coordinates coordinates = weather.coordinates;
if (coordinates.country == null && coordinates.city == null)
return "Unknown";
else if (coordinates.city == null)
return "Somewhere in " + coordinates.country;
else if (coordinates.country == null)
return coordinates.city;
return coordinates.city + ", " + coordinates.country;
}
}

View file

@ -1,67 +0,0 @@
package eu.m724.realweather.commands;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
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();
@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);
if (weather != null) {
long lastUpdate = playerWeatherCache.getLastUpdate(player);
colorize(sender, "\n&e" + weather.description);
if (weather.rainSeverity != null)
colorize(sender, "&6Rain: &b%s", weather.rainSeverity.toString());
if (weather.drizzleSeverity != null)
colorize(sender, "&6Drizzle: &b%s", weather.drizzleSeverity.toString());
if (weather.sleetSeverity != null)
colorize(sender, "&6Sleet: &b%s", weather.sleetSeverity.toString());
if (weather.snowSeverity != null)
colorize(sender, "&6Snow: &b%s", weather.snowSeverity.toString());
if (weather.thunderstormSeverity != null)
colorize(sender, "&6Thunderstorm: &b%s", weather.thunderstormSeverity.toString());
if (weather.shower)
colorize(sender, "&6Shower");
colorize(sender, "&6Cloudiness: &b%f%&7%", weather.cloudiness * 100);
colorize(sender, "&6Humidity: &b%f%&7%", weather.humidity * 100);
colorize(sender, "&6Temperature: &b%f&7°C (feels like %f°C)", weather.temperature - 273.15, weather.temperatureApparent - 273.15);
colorize(sender, "&6Wind: &b%f&7m/s (gust %fm/s)", weather.windSpeed, weather.windGust);
colorize(sender, "&6Last update: &b%s UTC\n", formatTime(lastUpdate));
} else {
colorize(sender, "&6No weather for you, try again in a second");
}
return true;
}
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

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

@ -1,11 +0,0 @@
package eu.m724.realweather.exception;
public class UserError extends Error {
private static final long serialVersionUID = 7152429719832602384L;
public UserError(String message) {
super(message);
}
}

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

@ -0,0 +1,90 @@
/*
* 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;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
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 CoordinatesLocationConverter coordinatesLocationConverter;
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 = 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, "");
} else {
sender.sendMessage("You can't run this command without arguments as console");
}
} else if (args.length >= 3) {
colorize(sender, "&cInvalid arguments, &7make sure it's &a\"/geo lat,lon\" &7or &a\"/geo x z\" &7or just &a\"/geo\"");
} else if (args.length == 2) {
double x, z;
try {
x = Double.parseDouble(args[0]);
z = Double.parseDouble(args[1]);
} catch (NumberFormatException e) {
colorize(sender, "&cInvalid arguments, &7make sure it's &a\"/geo lat,lon\" &7or &a\"/geo x z\" &7or just &a\"/geo\"");
return true;
}
Location location = new Location(null, x, 0, z);
Coordinates coordinates = coordinatesLocationConverter.locationToCoordinates(location);
colorize(sender, "");
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;
try {
String[] split = args[0].split(",");
latitude = Double.parseDouble(split[0]);
longitude = Double.parseDouble(split[1]);
} catch (NumberFormatException e) {
colorize(sender, "&cInvalid arguments, &7make sure it's &a\"/geo lat,lon\" &7or &a\"/geo x z\" &7or just &a\"/geo\"");
return true;
}
Coordinates coordinates = new Coordinates(latitude, longitude);
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, comma used as separator");
colorize(sender, "");
}
return true;
}
private void colorize(CommandSender sender, String text, Object... format) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', text.formatted(format)));
}
}

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,33 +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 int scaleLatitude;
public int 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.getInt("dimensions.latitude");
mapperConfig.scaleLongitude = configuration.getInt("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,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; package eu.m724.realweather.thunder;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
/** /**
* Configuration of the thunder module
* *
* @param enabled is thunder module enabled * @param enabled Whether the thunder module is enabled
* @param provider The provider name, may or may not exist, if it doesn't, an error is thrown later * @param provider The provider name
* @param refreshPeriod how often probe for strikes, in ticks * @param apiKey API key for the provider, null if not necessary
*/ */
public record ThunderConfig( public record ThunderConfig(
boolean enabled, boolean enabled,
String provider, String provider,
int refreshPeriod String apiKey
) { ) {
public static ThunderConfig fromConfiguration(ConfigurationSection configuration) { public static ThunderConfig fromConfiguration(ConfigurationSection configuration) {
return new ThunderConfig( return new ThunderConfig(
configuration.getBoolean("enabled"), configuration.getBoolean("enabled"),
configuration.getString("provider"), configuration.getString("provider"),
configuration.getInt("refreshPeriod") configuration.getString("apiKey")
); );
} }
} }

View file

@ -1,38 +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; package eu.m724.realweather.thunder;
import eu.m724.realweather.Configs;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.wtapi.provider.Providers; import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.wtapi.provider.exception.NoSuchProviderException; 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.exception.ProviderException;
import eu.m724.wtapi.provider.thunder.ThunderProvider; import eu.m724.wtapi.provider.thunder.ThunderProvider;
public class ThunderMaster { public class ThunderMaster {
private final ThunderConfig config = Configs.thunderConfig(); private final RealWeatherPlugin plugin;
private final String providerName;
private final String apiKey;
private ThunderProvider provider; private ThunderProvider provider;
/** public ThunderMaster(RealWeatherPlugin plugin, ThunderConfig config) {
this.plugin = plugin;
this.providerName = config.provider();
this.apiKey = config.apiKey();
}
/**
* initializes, tests and starts * initializes, tests and starts
*
* @throws ProviderException if provider initialization failed * @throws ProviderException if provider initialization failed
* @throws NoSuchProviderException config issue * @throws NoSuchProviderException config issue
*/ */
public void init(Plugin plugin) throws ProviderException, NoSuchProviderException { public void init() throws ProviderException, NoSuchProviderException {
if (!config.enabled()) provider = Providers.getThunderProvider(providerName, apiKey);
return;
provider = Providers.getThunderProvider(config.provider(), null);
provider.init();
ThunderTask thunderTask = new ThunderTask(provider); // TODO is this good? Probably not
thunderTask.init(); provider.registerStrikeConsumer(strike -> {
thunderTask.runTaskTimer(plugin, 0, config.refreshPeriod()); plugin.getServer().getPluginManager().callEvent(
new AsyncLightningStrikeEvent(strike)
DebugLogger.info("thunder loaded", 1); );
});
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.finer("Done initializing");
} }
public long getLatency() { public long getLatency() {
return provider.getLatency(); return provider.getLatency();
} }
// TODO should this be exposed?
public String getProviderName() {
return providerName;
}
} }

View file

@ -1,57 +0,0 @@
package eu.m724.realweather.thunder;
import java.util.ArrayList;
import org.bukkit.Location;
import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.wtapi.provider.thunder.ThunderProvider;
import eu.m724.wtapi.provider.thunder.impl.blitzortung.TimedStrike;
class ThunderTask extends BukkitRunnable {
private final ThunderProvider thunderProvider;
private final Mapper mapper = GlobalConstants.getMapper();
private final ArrayList<TimedStrike> strikes = new ArrayList<>();
public ThunderTask(ThunderProvider thunderProvider) {
this.thunderProvider = thunderProvider;
}
public void init() {
thunderProvider.registerStrikeHandler(coords -> {
strikes.add(new TimedStrike(System.currentTimeMillis() + thunderProvider.getDelay(), coords));
});
thunderProvider.start();
DebugLogger.info("thunderprovider started", 3);
}
@Override
public void run() {
DebugLogger.info("thundertask running", 3);
thunderProvider.tick();
while (!strikes.isEmpty()) {
TimedStrike strike = strikes.removeFirst();
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,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; package eu.m724.realweather.time;
import eu.m724.realweather.map.WorldList;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.wtapi.object.Coordinates; import eu.m724.wtapi.object.Coordinates;
public class AsyncPlayerTimeTask extends BukkitRunnable { public class AsyncPlayerTimeTask extends BukkitRunnable {
private final Server server = GlobalConstants.getPlugin().getServer();
private final Mapper mapper = GlobalConstants.getMapper();
private final TimeConverter timeConverter; 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.timeConverter = timeConverter;
this.server = server;
this.coordinatesLocationConverter = coordinatesLocationConverter;
this.worldList = worldList;
} }
@Override @Override
public void run() { public void run() {
for (Player player : server.getOnlinePlayers()) { for (Player player : server.getOnlinePlayers()) {
if (!player.hasPermission("realweather.dynamic")) continue; 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 time = timeConverter.calculateZoneOffset(coordinates.longitude());
long ticks = timeConverter.millisToTicks(time); long ticks = timeConverter.millisToTicks(time);
player.setPlayerTime(ticks, true); 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; package eu.m724.realweather.time;
import eu.m724.realweather.map.WorldList;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.mapper.Mapper;
/** /**
* This does world time, player time is basically offset of this, like timezone +0 * This does world time, player time is basically offset of this, like timezone +0
*/ */
public class SyncTimeUpdateTask extends BukkitRunnable { public class SyncTimeUpdateTask extends BukkitRunnable {
private final Mapper mapper = GlobalConstants.getMapper();
private final TimeConverter timeConverter; private final TimeConverter timeConverter;
private final WorldList worldList;
private final long zoneOffset; private final long zoneOffset;
SyncTimeUpdateTask(TimeConverter timeConverter, boolean dynamic) { SyncTimeUpdateTask(TimeConverter timeConverter, WorldList worldList, CoordinatesLocationConverter coordinatesLocationConverter, boolean dynamic) {
this.timeConverter = timeConverter; 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 @Override
@ -27,10 +34,9 @@ public class SyncTimeUpdateTask extends BukkitRunnable {
long ticks = timeConverter.millisToTicks(time + zoneOffset); 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)); worldList.getIncludedWorlds().forEach(world -> world.setFullTime(ticks));
// TODO add world handlers to mapper and don't calculate time each run // 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; package eu.m724.realweather.time;
import org.bukkit.configuration.ConfigurationSection; 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; package eu.m724.realweather.time;
public class TimeConverter { public class TimeConverter {
public final double scale; private final double scale;
public TimeConverter(double scale) { public TimeConverter(double scale) {
if (scale <= 0.0) {
throw new IllegalArgumentException("Scale must be greater than 0.0");
}
this.scale = scale; this.scale = scale;
} }
/** /**
* Divides time by predefined scale<br> * Divides time by predefined scale<br>
* ...slowing it down * ...slowing it down (or speeding up)
* *
* @param time unix milliseconds * @param time unix milliseconds
* @return scaled unix milliseconds * @return scaled unix milliseconds
@ -48,4 +57,8 @@ public class TimeConverter {
public long calculateZoneOffset(double longitude) { public long calculateZoneOffset(double longitude) {
return (long) (longitude * 240000); 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; package eu.m724.realweather.time;
import eu.m724.realweather.Configs; import eu.m724.realweather.RealWeatherPlugin;
import org.bukkit.GameRule;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.mapper.Mapper; import eu.m724.realweather.map.WorldList;
public class TimeMaster { public class TimeMaster {
private final Mapper mapper = GlobalConstants.getMapper(); private final RealWeatherPlugin plugin;
private final Plugin plugin = GlobalConstants.getPlugin(); private final TimeConverter timeConverter;
private final TimeConfig timeConfig = Configs.timeConfig();
// TODO I don't want to initialize this here private final boolean dynamic;
private final TimeConverter timeConverter = new TimeConverter(timeConfig.scale());
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() { public TimeConverter getTimeConverter() {
return timeConverter; return timeConverter;
} }
public void init() { public boolean isDynamic() {
if (!timeConfig.enabled()) return dynamic;
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);
} }
} }

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 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.TimeConverter;
import eu.m724.realweather.time.TimeMaster;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; 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 eu.m724.wtapi.object.Coordinates;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.ComponentBuilder;
public class LocalTimeCommand implements CommandExecutor { public class LocalTimeCommand implements CommandExecutor {
private final Mapper mapper = GlobalConstants.getMapper(); private final TimeMaster timeMaster;
private final TimeConfig timeConfig = Configs.timeConfig();
private final TimeConverter timeConverter; private final TimeConverter timeConverter;
private final CoordinatesLocationConverter coordinatesLocationConverter;
public LocalTimeCommand(TimeConverter timeConverter) { public LocalTimeCommand(TimeMaster timeMaster, CoordinatesLocationConverter coordinatesLocationConverter) {
this.timeConverter = timeConverter; this.coordinatesLocationConverter = coordinatesLocationConverter;
this.timeMaster = timeMaster;
this.timeConverter = timeMaster.getTimeConverter();
} }
@Override @Override
@ -46,10 +51,10 @@ public class LocalTimeCommand implements CommandExecutor {
sender.spigot().sendMessage(component); sender.spigot().sendMessage(component);
if (timeConfig.dynamic() && player != null && player.hasPermission("realweather.dynamic")) { if (timeMaster.isDynamic() && player != null && player.hasPermission("realweather.dynamic")) {
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation()); Coordinates coordinates = coordinatesLocationConverter.locationToCoordinates(player.getLocation());
long offsetTime = timeConverter.calculateZoneOffset(coordinates.longitude); long offsetTime = timeConverter.calculateZoneOffset(coordinates.longitude());
long offsetTimeTicks = timeConverter.millisToTicks(offsetTime); long offsetTimeTicks = timeConverter.millisToTicks(offsetTime);
boolean negative = offsetTime < 0; boolean negative = offsetTime < 0;

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; package eu.m724.realweather.updater;
import eu.m724.jarupdater.environment.ConstantEnvironment; import eu.m724.jarupdater.environment.ConstantEnvironment;
import eu.m724.jarupdater.updater.Updater; import eu.m724.jarupdater.updater.Updater;
import eu.m724.jarupdater.verify.SignatureVerifier; import eu.m724.jarupdater.verify.SignatureVerifier;
import eu.m724.jarupdater.verify.Verifier; 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.Downloader;
import eu.m724.jarupdater.download.SimpleDownloader; 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.GiteaMetadataDAO;
import eu.m724.jarupdater.live.MetadataDAO; import eu.m724.jarupdater.live.MetadataDAO;
import eu.m724.jarupdater.live.MetadataFacade; import eu.m724.jarupdater.live.MetadataFacade;
import eu.m724.realweather.RealWeatherPlugin;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
public class PluginUpdater extends Updater { public class PluginUpdater extends Updater {
private final UpdaterConfig updaterConfig = Configs.updaterConfig(); private PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
final Plugin plugin;
PluginUpdater(Plugin plugin, Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
super(environment, metadataProvider, downloader, 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( Environment environment = new ConstantEnvironment(
plugin.getDescription().getVersion(), plugin.getDescription().getVersion(),
Configs.updaterConfig().channel(), channel,
file.toPath() file.toPath()
); );
@ -46,13 +47,6 @@ public class PluginUpdater extends Updater {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return new PluginUpdater(plugin, environment, metadataFacade, downloader, verifier); return new PluginUpdater(environment, metadataFacade, downloader, verifier);
}
public void init() {
if (!updaterConfig.alert()) return;
UpdateNotifier updateNotifier = new UpdateNotifier(this, (version) -> {});
updateNotifier.register();
} }
} }

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; package eu.m724.realweather.updater;
import java.util.concurrent.CompletionException; 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.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@ -14,55 +20,51 @@ import eu.m724.jarupdater.object.Version;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
public class UpdateNotifier extends BukkitRunnable implements Listener { // TODO move this to jarupdater public class UpdateNotifier extends BukkitRunnable implements Listener { // TODO move this to jarupdater
private final Plugin plugin = RealWeatherPlugin.getInstance();
private final PluginUpdater updater; private final PluginUpdater updater;
private final Consumer<Version> updateConsumer;
private final Plugin plugin;
private Version latestVersion; private Version latestVersion;
public UpdateNotifier(PluginUpdater updater, Consumer<Version> updateConsumer) { public UpdateNotifier(PluginUpdater updater) {
this.updater = updater; this.updater = updater;
this.updateConsumer = updateConsumer;
this.plugin = updater.plugin;
} }
public void register() { public void register() {
this.runTaskTimerAsynchronously(updater.plugin, 0, 432000); // 6h this.runTaskTimerAsynchronously(plugin, 0, 6 * 3600 * 20); // 6h
plugin.getServer().getPluginManager().registerEvents(this, plugin); plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@Override @Override
public void run() { public void run() {
DebugLogger.info("update task running", 2); DebugLogger.finer("Updater running");
try { try {
latestVersion = updater.getLatestVersion().join(); latestVersion = updater.getLatestVersion().join();
} catch (CompletionException e) { } catch (CompletionException e) {
Throwable ex = e.getCause(); DebugLogger.warning("Error trying to contact update server:");
DebugLogger.info("Error trying to contact update server: %s", 0, ex.getMessage()); DebugLogger.warning(" %s", e.getCause().toString());
if (DebugLogger.getDebugLevel() >= 1)
e.printStackTrace();
} }
if (latestVersion == null) return; if (latestVersion == null) {
DebugLogger.info("RealWeather is outdated. /rwadmin update", 0); 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")) { 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 @EventHandler
public void onPlayerJoin(PlayerJoinEvent e) { public void onPlayerJoin(PlayerJoinEvent e) {
Player player = e.getPlayer(); Player player = e.getPlayer();
if (latestVersion != null && player.hasPermission("realweather.update.notify")) { 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,137 +1,105 @@
/*
* 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; package eu.m724.realweather.weather;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import org.bukkit.Server; import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.api.weather.AsyncPlayerWeatherUpdateEvent;
import eu.m724.wtapi.provider.weather.WeatherQueryResult;
import org.bukkit.World;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.map.CoordinatesLocationConverter;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.realweather.api.weather.AsyncWeatherUpdateEvent;
import eu.m724.wtapi.object.Coordinates; import eu.m724.wtapi.object.Coordinates;
import eu.m724.wtapi.object.Weather; import eu.m724.wtapi.object.Weather;
import eu.m724.wtapi.provider.weather.WeatherProvider; 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 WeatherProvider weatherProvider;
private final PlayerWeatherStore playerWeatherStore;
private final Mapper mapper = GlobalConstants.getMapper(); private final CoordinatesLocationConverter coordinatesLocationConverter;
private final Plugin plugin = GlobalConstants.getPlugin(); private final List<World> worlds;
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<>();
public DynamicWeatherRetriever(WeatherProvider weatherProvider) { public DynamicWeatherRetriever(RealWeatherPlugin plugin, WeatherProvider weatherProvider, PlayerWeatherStore playerWeatherStore) {
this.plugin = plugin;
this.weatherProvider = weatherProvider; this.weatherProvider = weatherProvider;
} this.playerWeatherStore = playerWeatherStore;
private record CoordinatesResult( this.coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
Map<Coordinates, Player[]> coordinatesPlayersMap, this.worlds = plugin.getWorldList().getIncludedWorlds();
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);
} }
@Override @Override
public void run() { public void run() {
DebugLogger.info("Weather retrieval", 3); DebugLogger.finer("Updating weather");
long now = System.currentTimeMillis(); LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
CoordinatesResult coordinates; float maxHourlyUpdates = (float) weatherProvider.getQuota().getHourlyQuota() / plugin.getServer().getOnlinePlayers().size();
if (now > nextUpdate) { long updateDelay = Math.max(60, (long) (3600 / maxHourlyUpdates));
coordinates = makeCoordinates(server.getOnlinePlayers());
// calculate acceptable request rate based on weather provider quota and active players DebugLogger.finer("Update delay: %d seconds", updateDelay);
float hourly = weatherProvider.getQuotaHourly() / (float)(weatherProvider.getBulkLimit() * coordinates.coordinatesCount());
nextUpdate = now + Math.max(60000, (long) (3600000 / hourly)); Player[] playersToUpdate = plugin.getServer().getOnlinePlayers().stream().filter(player -> {
DebugLogger.info("Next update in %d", 3, nextUpdate); LocalDateTime lastUpdate = playerWeatherStore.getLastUpdate(player);
} else { // immediate update for those that need it right now
if (neededUpdate.isEmpty()) return; if (!player.hasPermission("realweather.dynamic")) return false;
DebugLogger.info("Players in need of update: %d", 2, neededUpdate.size()); if (!worlds.contains(player.getWorld())) return false;
coordinates = makeCoordinates(neededUpdate);
neededUpdate.clear(); 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 = coordinates.coordinatesPlayersMap().keySet().toArray(Coordinates[]::new);
// TODO change to Collection in wtapi? but some ordered kind Coordinates[] coordinatesArray = Arrays.stream(playersToUpdate).map(player ->
CompletableFuture<Weather[]> weathersFuture = coordinatesLocationConverter.locationToCoordinates(player.getLocation())
weatherProvider.getWeatherBulk(coordinatesArray); ).toArray(Coordinates[]::new);
try { CompletableFuture<WeatherQueryResult> weathersFuture =
Weather[] weathers = weathersFuture.join(); weatherProvider.getWeather(coordinatesArray);
for (int i=0; i<weathers.length; i++) {
Weather weather = weathers[i];
for (Player player : coordinates.coordinatesPlayersMap().get(coordinatesArray[i])) {
playerWeatherCache.put(player, weather, now);
AsyncWeatherUpdateEvent event = WeatherQueryResult result = weathersFuture.join();
new AsyncWeatherUpdateEvent(player, weather);
server.getPluginManager().callEvent(event); if (result.exception() != null) {
} DebugLogger.severe("An error has occurred retrieving weather data");
} DebugLogger.warning(" " + result.exception());
} catch (CompletionException e) { // TODO handle finer exceptions
DebugLogger.info("An error occurred trying to retrieve weather data", 0);
if (DebugLogger.getDebugLevel() > 0) return;
e.printStackTrace();
} }
DebugLogger.info("dynamic retriever done", 3); Weather[] weathers = result.weathers();
}
for (int i=0; i<weathers.length; i++) {
@EventHandler Weather weather = weathers[i];
public void onPlayerJoin(PlayerJoinEvent event) { Player player = playersToUpdate[i];
Player player = event.getPlayer();
neededUpdate.add(player); playerWeatherStore.put(player, weather);
AsyncPlayerWeatherUpdateEvent event =
new AsyncPlayerWeatherUpdateEvent(player, weather);
plugin.getServer().getPluginManager().callEvent(event);
}
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,47 +1,53 @@
/*
* 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; package eu.m724.realweather.weather;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.api.weather.AsyncWeatherUpdateEvent; 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.Coordinates;
import eu.m724.wtapi.object.Weather; import eu.m724.wtapi.object.Weather;
import eu.m724.wtapi.provider.weather.WeatherProvider; import eu.m724.wtapi.provider.weather.WeatherProvider;
import org.bukkit.plugin.Plugin; import eu.m724.wtapi.provider.weather.WeatherQueryResult;
import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitRunnable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
public class StaticWeatherRetriever extends BukkitRunnable { public class StaticWeatherRetriever extends BukkitRunnable {
private final Plugin plugin = GlobalConstants.getPlugin(); private final RealWeatherPlugin plugin;
private final Mapper mapper = GlobalConstants.getMapper();
private final WeatherProvider weatherProvider; 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.weatherProvider = weatherProvider;
this.coordinatesLocationConverter = plugin.getCoordinatesLocationConverter();
} }
@Override @Override
public void run() { public void run() {
Coordinates point = mapper.getPoint(); DebugLogger.finer("Updating weather");
CompletableFuture<Weather> weatherFuture = weatherProvider.getWeather(point);
try { Coordinates point = coordinatesLocationConverter.getStaticPoint();
Weather weather = weatherFuture.join();
AsyncWeatherUpdateEvent event = WeatherQueryResult result = weatherProvider.getWeather(point).join();
new AsyncWeatherUpdateEvent(null, weather);
plugin.getServer().getPluginManager().callEvent(event); if (result.exception() != null) {
} catch (CompletionException e) { // TODO handle finer exceptions DebugLogger.severe("An error has occurred retrieving weather data");
DebugLogger.info("An error occurred trying to retrieve weather data", 0); DebugLogger.warning(" " + result.exception());
if (DebugLogger.getDebugLevel() > 0) return;
e.printStackTrace();
} }
DebugLogger.info("static weather retriever is done", 3); Weather weather = result.weathers()[0];
AsyncGlobalWeatherUpdateEvent event =
new AsyncGlobalWeatherUpdateEvent(weather);
plugin.getServer().getPluginManager().callEvent(event);
DebugLogger.fine("Done updating weather");
} }
} }

View file

@ -1,9 +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; package eu.m724.realweather.weather;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.api.weather.AsyncGlobalWeatherUpdateEvent;
import eu.m724.realweather.api.weather.AsyncWeatherUpdateEvent; 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 eu.m724.wtapi.object.Weather;
import org.bukkit.WeatherType; import org.bukkit.WeatherType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -11,41 +16,55 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
// TODO make weather more comprehensive // TODO make weather more intricate
public class WeatherChanger implements Listener { 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) @EventHandler(priority = EventPriority.LOWEST)
public void onWeatherUpdate(AsyncWeatherUpdateEvent event) { public void onGlobalWeatherUpdate(AsyncGlobalWeatherUpdateEvent event) {
if (event.isCancelled()) return;
Weather weather = event.getWeather();
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);
}
});
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerWeatherUpdate(AsyncPlayerWeatherUpdateEvent event) {
if (event.isCancelled()) return;
Player player = event.getPlayer(); Player player = event.getPlayer();
Weather weather = event.getWeather(); Weather weather = event.getWeather();
if (player != null) { // dynamic mode if (weather.thundering() || weather.snowing() || weather.raining()) {
DebugLogger.info("Changing weather for player %s", 2, player.getName()); DebugLogger.finer("Changing weather for player %s to downfall", player.getName());
player.setPlayerWeather(WeatherType.DOWNFALL);
if (weather.isThundering() || weather.isSnowing() || weather.isRaining()) { } else {
player.setPlayerWeather(WeatherType.DOWNFALL); DebugLogger.finer("Changing weather for player %s to clear", player.getName());
} else { player.setPlayerWeather(WeatherType.CLEAR);
player.setPlayerWeather(WeatherType.CLEAR);
}
} else { // static mode
DebugLogger.info("Changing weather static", 3);
mapper.getWorlds().forEach(w -> {
DebugLogger.info("Changing weather static in world %s", 2, w.getName());
if (weather.isThundering()) {
w.setClearWeatherDuration(0);
w.setWeatherDuration(120000);
w.setThunderDuration(120000);
} else if (weather.isRaining() || weather.isSnowing()) {
w.setClearWeatherDuration(0);
w.setWeatherDuration(120000);
w.setThunderDuration(0);
} else {
w.setClearWeatherDuration(120000);
w.setWeatherDuration(0);
w.setThunderDuration(0);
}
});
} }
} }
} }

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; package eu.m724.realweather.weather;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
@ -5,15 +10,15 @@ import org.bukkit.configuration.ConfigurationSection;
/** /**
* Configuration of the weather module * Configuration of the weather module
* *
* @param enabled Is weather module enabled * @param enabled Whether the weather module is enabled
* @param provider The provider name, may or may not exist, if it doesn't, an error is thrown later * @param provider The provider name
* @param apiKey API key for the provider * @param apiKey API key for the provider, null if not necessary
* @param dynamic dynamic mode, weather is per player or global * @param dynamic dynamic mode, weather is per player or global
*/ */
public record WeatherConfig( public record WeatherConfig(
boolean enabled, boolean enabled,
String provider, 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 boolean dynamic
) { ) {
public static WeatherConfig fromConfiguration(ConfigurationSection configuration) { 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; package eu.m724.realweather.weather;
import eu.m724.realweather.Configs;
import org.bukkit.GameRule;
import org.bukkit.plugin.Plugin;
import eu.m724.realweather.DebugLogger; import eu.m724.realweather.DebugLogger;
import eu.m724.realweather.GlobalConstants; import eu.m724.realweather.RealWeatherPlugin;
import eu.m724.realweather.mapper.Mapper;
import eu.m724.wtapi.provider.Providers;
import eu.m724.wtapi.provider.exception.NoSuchProviderException; 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.exception.ProviderException;
import eu.m724.wtapi.provider.weather.WeatherProvider; import eu.m724.wtapi.provider.weather.WeatherProvider;
public class WeatherMaster { public class WeatherMaster {
private final WeatherConfig config = Configs.weatherConfig(); private final RealWeatherPlugin plugin;
private final Mapper mapper = GlobalConstants.getMapper(); 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 * initializes, tests and starts
* @throws ProviderException if provider initialization failed * @throws ProviderException if provider initialization failed
* @throws NoSuchProviderException config issue * @throws NoSuchProviderException config issue
*/ */
public void init(Plugin plugin) throws ProviderException, NoSuchProviderException { public void init() throws ProviderException, NoSuchProviderException {
if (!config.enabled()) { WeatherProvider provider = Providers.getWeatherProvider(providerName, apiKey);
DebugLogger.info("weather module is disabled", 1);
return;
}
WeatherProvider provider = Providers.getWeatherProvider(config.provider(), config.apiKey());
provider.init(); provider.init();
if (config.dynamic()) { if (dynamic) {
DynamicWeatherRetriever retriever = new DynamicWeatherRetriever(provider); DebugLogger.finer("Weather is dynamic");
retriever.runTaskTimerAsynchronously(plugin,0, 1000);
plugin.getServer().getPluginManager().registerEvents(retriever, plugin); DynamicWeatherRetriever retriever = new DynamicWeatherRetriever(plugin, provider, playerWeatherStore);
retriever.runTaskTimerAsynchronously(plugin,0, 200);
} else { } else {
StaticWeatherRetriever retriever = new StaticWeatherRetriever(provider); DebugLogger.finer("Weather is static");
StaticWeatherRetriever retriever = new StaticWeatherRetriever(plugin, provider);
retriever.runTaskTimerAsynchronously(plugin,0, 60000); retriever.runTaskTimerAsynchronously(plugin,0, 60000);
} }
plugin.getServer().getPluginManager().registerEvents(new WeatherChanger(), plugin); plugin.getServer().getPluginManager().registerEvents(new WeatherChanger(plugin.getWorldList()), plugin);
mapper.registerWorldLoadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, false)); DebugLogger.finer("Done initializing");
mapper.registerWorldUnloadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, true));
// TODO replace that
DebugLogger.info("weather loaded", 1); // 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

@ -0,0 +1,69 @@
/*
* 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.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.wtapi.object.Weather;
public class LocalWeatherCommand implements CommandExecutor {
private final PlayerWeatherStore playerWeatherStore;
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 = playerWeatherStore.getWeather(player);
if (weather != null) {
LocalDateTime lastUpdate = playerWeatherStore.getLastUpdate(player);
colorize(sender, "&6Weather for: &b%f&7, &b%f &7(lat, lon)\n", weather.coordinates().latitude(), weather.coordinates().longitude());
List<String> states = new ArrayList<>(3);
if (weather.raining()) states.add("Raining");
if (weather.thundering()) states.add("Thundering");
if (weather.thundering()) states.add("Snowing");
if (!states.isEmpty()) {
colorize(sender, "&6" + String.join(", ", states));
}
colorize(sender, "&6Temperature: &b%.1f&7°C (feels like %.1f°C)", weather.temperatureCelsius(), weather.temperatureApparentCelsius());
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", lastUpdate.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
} else {
colorize(sender, "&6No weather for you yet, try again in a few seconds");
}
return true;
}
private void colorize(CommandSender sender, String text, Object... format) {
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', text.formatted(format)));
}
}

View file

@ -2,22 +2,11 @@
### GENERAL SETTINGS ### ### GENERAL SETTINGS ###
############################ ############################
# Master switch # Not much to be found here! Explore the other files.
enabled: false
updater: updater:
# Notify players and console about plugin updates # Notify players and console about plugin updates
# This also controls automatic checking # This also controls automatic checking
# You can still update with /rwadmin update # You can still update with /rwadmin update
# Relevant permission node: realweather.update.notify # Relevant permission node: realweather.update.notify
notify: true 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

View file

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

View file

@ -6,10 +6,7 @@ enabled: false
# Currently only blitzortung # Currently only blitzortung
provider: blitzortung provider: blitzortung
# No API key needed
# How often should we poll for updates and spawn lightning # Lightning is DYNAMIC.
# This is a synchronous task # Settings are in map.yml
# 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

View file

@ -2,20 +2,19 @@
### TIME SETTINGS ### ### TIME SETTINGS ###
############################ ############################
# Warning: this removes sleep # Warning: this removes sleep. No, not a bug, as you can't skip time IRL.
# 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?
enabled: false enabled: false
# How this plugin affects your world: # How time is applied:
# - static (false): time is the same across the world # - 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 # Settings for both are in map.yml
dynamic: true dynamic: true
# x in game day cycles in 1 irl day cycle # Real days per in-game day
# 2.0 - time goes 2x SLOWER # 2.0 - time goes 2x SLOWER
# 0.5 - time goes 2x FASTER # 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 scale: 1.0

View file

@ -9,13 +9,12 @@
enabled: false enabled: false
# Currently only OpenWeatherMap # Currently only OpenMeteo
provider: openweathermap provider: openmeteo
# put your OpenWeatherMap api key # No API key needed
apiKey: REPLACE ME
# How this plugin affects your world: # How weather is applied:
# - static (false): weather is the same across the world # - 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 # - 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 # Settings for both are in map.yml
dynamic: true dynamic: true

View file

@ -4,12 +4,13 @@ version: ${project.version}
author: Minecon724 author: Minecon724
website: https://www.spigotmc.org/resources/realweather-realtime.101599/ website: https://www.spigotmc.org/resources/realweather-realtime.101599/
api-version: 1.19.4 api-version: 1.16
load: STARTUP load: STARTUP
main: eu.m724.realweather.RealWeatherPlugin main: eu.m724.realweather.RealWeatherPlugin
libraries: libraries:
- org.java-websocket:Java-WebSocket:1.5.7 - org.java-websocket:Java-WebSocket:1.6.0
- eu.m724:mstats-spigot:0.1.2
commands: commands:
rwadmin: rwadmin: