work
player time is not working, idk why yet
This commit is contained in:
parent
8931f32527
commit
ea9a424fda
12 changed files with 192 additions and 30 deletions
|
@ -62,3 +62,11 @@ solution? for now let's warn and update time every tick
|
||||||
to check:
|
to check:
|
||||||
scale * floor(72/scale) == 72
|
scale * floor(72/scale) == 72
|
||||||
|
|
||||||
|
|
||||||
|
goal: offsetting by player position
|
||||||
|
|
||||||
|
t = (longitude / 15) * 1000 * scale
|
||||||
|
|
||||||
|
accounting for sunrise and sunset
|
||||||
|
TODO, idk yet without
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.bukkit.plugin.java.JavaPlugin;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
|
|
||||||
import eu.m724.realweather.commands.GeoCommand;
|
import eu.m724.realweather.commands.GeoCommand;
|
||||||
|
import eu.m724.realweather.commands.LocalTimeCommand;
|
||||||
import eu.m724.realweather.mapper.Mapper;
|
import eu.m724.realweather.mapper.Mapper;
|
||||||
import eu.m724.realweather.mapper.MapperConfig;
|
import eu.m724.realweather.mapper.MapperConfig;
|
||||||
import eu.m724.realweather.mapper.MapperEventHandler;
|
import eu.m724.realweather.mapper.MapperEventHandler;
|
||||||
|
@ -30,11 +31,8 @@ public class RealWeatherPlugin extends JavaPlugin {
|
||||||
private ThunderMaster thunderMaster;
|
private ThunderMaster thunderMaster;
|
||||||
private TimeMaster timeMaster;
|
private TimeMaster timeMaster;
|
||||||
|
|
||||||
private YamlConfiguration config;
|
|
||||||
private Logger logger;
|
private Logger logger;
|
||||||
|
|
||||||
private YamlConfiguration mapConfig;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
logger = getLogger();
|
logger = getLogger();
|
||||||
|
@ -116,6 +114,9 @@ public class RealWeatherPlugin extends JavaPlugin {
|
||||||
|
|
||||||
getCommand("geo").setExecutor(new GeoCommand());
|
getCommand("geo").setExecutor(new GeoCommand());
|
||||||
|
|
||||||
|
if (GlobalConstants.timeConfig.enabled)
|
||||||
|
getCommand("localtime").setExecutor(new LocalTimeCommand());
|
||||||
|
|
||||||
DebugLogger.info("ended loading", 1);
|
DebugLogger.info("ended loading", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class GeoCommand implements CommandExecutor {
|
||||||
}
|
}
|
||||||
} else if (args.length >= 2) {
|
} else if (args.length >= 2) {
|
||||||
double latitude = Double.parseDouble(args[0]);
|
double latitude = Double.parseDouble(args[0]);
|
||||||
double longitude = Double.parseDouble(args[0]);
|
double longitude = Double.parseDouble(args[1]);
|
||||||
|
|
||||||
Coordinates coordinates = new Coordinates(latitude, longitude);
|
Coordinates coordinates = new Coordinates(latitude, longitude);
|
||||||
Location location = GlobalConstants.getMapper().coordinatesToLocation(player.getWorld(), coordinates);
|
Location location = GlobalConstants.getMapper().coordinatesToLocation(player.getWorld(), coordinates);
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package eu.m724.realweather.commands;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.time.temporal.TemporalUnit;
|
||||||
|
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import eu.m724.realweather.GlobalConstants;
|
||||||
|
import eu.m724.realweather.mapper.Mapper;
|
||||||
|
import eu.m724.realweather.time.TimeConfig;
|
||||||
|
import eu.m724.wtapi.object.Coordinates;
|
||||||
|
import net.md_5.bungee.api.ChatColor;
|
||||||
|
import net.md_5.bungee.api.chat.BaseComponent;
|
||||||
|
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||||
|
|
||||||
|
public class LocalTimeCommand implements CommandExecutor {
|
||||||
|
private TimeConfig timeConfig = GlobalConstants.getTimeConfig();
|
||||||
|
private Mapper mapper = GlobalConstants.getMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
Player player = sender instanceof Player ? (Player) sender : null;
|
||||||
|
|
||||||
|
long worldTime = timeConfig.calculateWorldTimeSeconds();
|
||||||
|
Duration worldTimeDuration = Duration.ofSeconds(worldTime);
|
||||||
|
|
||||||
|
String worldTimeFormatted = String.format("%d:%02d:%02d",
|
||||||
|
worldTimeDuration.toHours(),
|
||||||
|
worldTimeDuration.toMinutesPart(),
|
||||||
|
worldTimeDuration.toSecondsPart());
|
||||||
|
|
||||||
|
BaseComponent[] component = new ComponentBuilder("World time: ").color(ChatColor.GRAY)
|
||||||
|
.append(worldTimeFormatted).color(ChatColor.DARK_AQUA)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
sender.spigot().sendMessage(component);
|
||||||
|
|
||||||
|
if (timeConfig.dynamic && player != null && player.hasPermission("realweather.dynamic")) {
|
||||||
|
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation());
|
||||||
|
|
||||||
|
long offsetTime = timeConfig.calculateTimeOffsetSeconds(coordinates.longitude);
|
||||||
|
boolean negative = offsetTime < 0;
|
||||||
|
|
||||||
|
Duration localTimeDuration = worldTimeDuration.plus(offsetTime, ChronoUnit.SECONDS);
|
||||||
|
Duration offsetTimeDuration = Duration.ofSeconds(negative ? -offsetTime : offsetTime);
|
||||||
|
|
||||||
|
String offsetTimeFormatted = String.format("%s%d:%02d:%02d",
|
||||||
|
(negative ? "-" : "+"),
|
||||||
|
offsetTimeDuration.toHours(),
|
||||||
|
offsetTimeDuration.toMinutesPart(),
|
||||||
|
offsetTimeDuration.toSecondsPart());
|
||||||
|
|
||||||
|
String localTimeFormatted = String.format("%d:%02d:%02d",
|
||||||
|
localTimeDuration.toHours(),
|
||||||
|
localTimeDuration.toMinutesPart(),
|
||||||
|
localTimeDuration.toSecondsPart());
|
||||||
|
|
||||||
|
component = new ComponentBuilder("Local time: ").color(ChatColor.GOLD)
|
||||||
|
.append(localTimeFormatted).color(ChatColor.AQUA)
|
||||||
|
.append(" " + offsetTimeFormatted).color(ChatColor.GRAY)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
sender.spigot().sendMessage(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,8 +1,13 @@
|
||||||
package eu.m724.realweather.mapper;
|
package eu.m724.realweather.mapper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.bukkit.GameRule;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
@ -13,10 +18,35 @@ public class Mapper {
|
||||||
private MapperConfig config;
|
private MapperConfig config;
|
||||||
private List<World> worlds = new ArrayList<>();
|
private List<World> worlds = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Consumer<World>> worldLoadConsumers = new ArrayList<>();
|
||||||
|
private List<Consumer<World>> worldUnloadConsumers = new ArrayList<>();
|
||||||
|
// TODO game rules
|
||||||
|
|
||||||
public Mapper(MapperConfig config) {
|
public Mapper(MapperConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a consumer which will be called on world load
|
||||||
|
* @param consumer
|
||||||
|
*/
|
||||||
|
public void registerWorldLoadConsumer(Consumer<World> consumer) {
|
||||||
|
this.worldLoadConsumers.add(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a consumer which will be called on world unload
|
||||||
|
* @param consumer
|
||||||
|
*/
|
||||||
|
public void registerWorldUnloadConsumer(Consumer<World> consumer) {
|
||||||
|
this.worldUnloadConsumers.add(consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers events handled by mapper.
|
||||||
|
* This should be called once on plugin load.
|
||||||
|
* @param plugin
|
||||||
|
*/
|
||||||
public void registerEvents(Plugin plugin) {
|
public void registerEvents(Plugin plugin) {
|
||||||
plugin.getServer().getPluginManager().registerEvents(new MapperEventHandler(this), plugin);
|
plugin.getServer().getPluginManager().registerEvents(new MapperEventHandler(this), plugin);
|
||||||
}
|
}
|
||||||
|
@ -29,8 +59,8 @@ public class Mapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Location coordinatesToLocation(World world, Coordinates coordinates) {
|
public Location coordinatesToLocation(World world, Coordinates coordinates) {
|
||||||
double x = -coordinates.latitude * config.scaleLatitude;
|
double x = coordinates.longitude * config.scaleLongitude;
|
||||||
double z = coordinates.longitude * config.scaleLongitude;
|
double z = -coordinates.latitude * config.scaleLatitude;
|
||||||
|
|
||||||
return new Location(world, x, 0, z);
|
return new Location(world, x, 0, z);
|
||||||
|
|
||||||
|
@ -47,14 +77,18 @@ public class Mapper {
|
||||||
boolean loadWorld(World world) {
|
boolean loadWorld(World world) {
|
||||||
boolean loaded = config.worlds.contains(world.getName()) ^ config.worldBlacklist;
|
boolean loaded = config.worlds.contains(world.getName()) ^ config.worldBlacklist;
|
||||||
|
|
||||||
if (loaded)
|
if (loaded) {
|
||||||
worlds.add(world);
|
worlds.add(world);
|
||||||
|
worldLoadConsumers.forEach(consumer -> consumer.accept(world));
|
||||||
|
}
|
||||||
|
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
void unloadWorld(World world) {
|
void unloadWorld(World world) {
|
||||||
worlds.remove(world);
|
if (worlds.remove(world)) {
|
||||||
|
worldUnloadConsumers.forEach(consumer -> consumer.accept(world));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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.GlobalConstants;
|
import eu.m724.realweather.GlobalConstants;
|
||||||
import eu.m724.realweather.mapper.Mapper;
|
import eu.m724.realweather.mapper.Mapper;
|
||||||
import eu.m724.realweather.weather.PlayerWeatherDirectory;
|
import eu.m724.realweather.weather.PlayerWeatherDirectory;
|
||||||
|
@ -27,15 +28,10 @@ public class AsyncPlayerTimeTask extends BukkitRunnable {
|
||||||
for (Player player : server.getOnlinePlayers()) {
|
for (Player player : server.getOnlinePlayers()) {
|
||||||
if (!player.hasPermission("realweather.dynamic")) continue;
|
if (!player.hasPermission("realweather.dynamic")) continue;
|
||||||
|
|
||||||
Weather weather = playerWeatherDirectory.getWeather(player);
|
|
||||||
|
|
||||||
if (weather != null) {
|
|
||||||
// TODO sunrise sunset
|
|
||||||
}
|
|
||||||
|
|
||||||
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation());
|
Coordinates coordinates = mapper.locationToCoordinates(player.getLocation());
|
||||||
long offsetTicks = (long) ((coordinates.longitude / 15) * 1000 * timeConfig.scale);
|
long offsetTicks = (long) ((coordinates.longitude / 15) * 1000 * timeConfig.scale);
|
||||||
player.setPlayerTime(offsetTicks, true); // TODO can this be negative?
|
player.setPlayerTime(offsetTicks, true); // TODO can this be negative?
|
||||||
|
DebugLogger.info("Time for %s: %d", 2, player.getName(), offsetTicks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package eu.m724.realweather.time;
|
||||||
|
|
||||||
import org.bukkit.scheduler.BukkitRunnable;
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
|
import eu.m724.realweather.DebugLogger;
|
||||||
import eu.m724.realweather.GlobalConstants;
|
import eu.m724.realweather.GlobalConstants;
|
||||||
import eu.m724.realweather.mapper.Mapper;
|
import eu.m724.realweather.mapper.Mapper;
|
||||||
|
|
||||||
|
@ -16,10 +17,10 @@ public class SyncTimeUpdateTask extends BukkitRunnable {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// TODO double?
|
// TODO double?
|
||||||
long now = (long) (System.currentTimeMillis() * timeConfig.scale / 1000);
|
long ticks = timeConfig.calculateWorldTimeTicks();
|
||||||
long time = Math.floorMod(now % 86400 / 72 * 20 - 6000, 24000);
|
DebugLogger.info("Updating time: %d", 2, ticks);
|
||||||
|
|
||||||
mapper.getWorlds().forEach(world -> world.setFullTime(time));
|
mapper.getWorlds().forEach(world -> world.setFullTime(ticks));
|
||||||
// TODO add world handlers to mapper and don't calculate time each run
|
// TODO add world handlers to mapper and don't calculate time each run
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,34 @@ public class TimeConfig {
|
||||||
|
|
||||||
return timeConfig;
|
return timeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long calculateWorldTimeSeconds() {
|
||||||
|
long now = (long) (System.currentTimeMillis() * scale / 1000) % 86400;
|
||||||
|
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long calculateWorldTimeTicks() {
|
||||||
|
long now = calculateWorldTimeSeconds();
|
||||||
|
long ticks = Math.floorMod(now / 72 * 20 - 6000, 24000);
|
||||||
|
|
||||||
|
return ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this method is mostly for info
|
||||||
|
* @param longitude
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public long calculateTimeOffsetSeconds(double longitude) {
|
||||||
|
long offset = (long) ((longitude / 15) * 3600 * scale);
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long calculateTimeOffsetTicks(double longitude) {
|
||||||
|
long offsetTicks = (long) ((longitude / 15) * 1000 * scale);
|
||||||
|
|
||||||
|
return offsetTicks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
package eu.m724.realweather.time;
|
package eu.m724.realweather.time;
|
||||||
|
|
||||||
|
import org.bukkit.GameRule;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
import eu.m724.realweather.DebugLogger;
|
import eu.m724.realweather.DebugLogger;
|
||||||
import eu.m724.realweather.GlobalConstants;
|
import eu.m724.realweather.GlobalConstants;
|
||||||
|
import eu.m724.realweather.commands.LocalTimeCommand;
|
||||||
import eu.m724.realweather.mapper.Mapper;
|
import eu.m724.realweather.mapper.Mapper;
|
||||||
import eu.m724.realweather.object.UserException;
|
import eu.m724.realweather.object.UserException;
|
||||||
|
|
||||||
public class TimeMaster {
|
public class TimeMaster {
|
||||||
private TimeConfig config;
|
private TimeConfig timeConfig;
|
||||||
private Mapper mapper = GlobalConstants.getMapper();
|
private Mapper mapper = GlobalConstants.getMapper();
|
||||||
private Plugin plugin = GlobalConstants.getPlugin();
|
private Plugin plugin = GlobalConstants.getPlugin();
|
||||||
|
|
||||||
public TimeMaster(TimeConfig config) {
|
public TimeMaster(TimeConfig timeConfig) {
|
||||||
this.config = config;
|
this.timeConfig = timeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,21 +23,27 @@ public class TimeMaster {
|
||||||
* @throws UserException config issue
|
* @throws UserException config issue
|
||||||
*/
|
*/
|
||||||
public void init() throws UserException {
|
public void init() throws UserException {
|
||||||
if (!config.enabled)
|
if (!timeConfig.enabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
long period = (long) (72 / config.scale);
|
long period = (long) (72 / timeConfig.scale);
|
||||||
|
|
||||||
if (config.scale * Math.floor(period) == 72) {
|
if (timeConfig.scale * Math.floor(period) != 72.0) {
|
||||||
// TODO log this properly
|
// TODO log this properly
|
||||||
DebugLogger.info("Warning: RealTime scale is not optimal. Time will be out of sync.", 0);
|
DebugLogger.info("Warning: RealTime scale is not optimal. Time will be out of sync.", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
new SyncTimeUpdateTask(config).runTaskTimer(plugin, 0, period);
|
new SyncTimeUpdateTask(timeConfig).runTaskTimer(plugin, 0, period);
|
||||||
new AsyncPlayerTimeTask(config).runTaskTimerAsynchronously(plugin, 0, period); // TODO maybe use a different period?
|
|
||||||
// TODO also make it on player join
|
|
||||||
|
|
||||||
DebugLogger.info("time loaded", 1);
|
if (timeConfig.dynamic)
|
||||||
|
new AsyncPlayerTimeTask(timeConfig).runTaskTimerAsynchronously(plugin, 0, period);
|
||||||
|
// TODO maybe use a different period?
|
||||||
|
// TODO also make it on player join
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.m724.realweather.weather;
|
package eu.m724.realweather.weather;
|
||||||
|
|
||||||
|
import org.bukkit.GameRule;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
import eu.m724.realweather.DebugLogger;
|
import eu.m724.realweather.DebugLogger;
|
||||||
|
@ -40,6 +41,9 @@ public class WeatherMaster {
|
||||||
retriever.runTaskAsynchronously(plugin);
|
retriever.runTaskAsynchronously(plugin);
|
||||||
plugin.getServer().getPluginManager().registerEvents(retriever, plugin);
|
plugin.getServer().getPluginManager().registerEvents(retriever, plugin);
|
||||||
|
|
||||||
|
mapper.registerWorldLoadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, false));
|
||||||
|
mapper.registerWorldUnloadConsumer(world -> world.setGameRule(GameRule.DO_WEATHER_CYCLE, true));
|
||||||
|
|
||||||
DebugLogger.info("weather loaded", 1);
|
DebugLogger.info("weather loaded", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ enabled: false
|
||||||
# settings for both are in map.yml
|
# settings for both are in map.yml
|
||||||
dynamic: true
|
dynamic: true
|
||||||
|
|
||||||
# x day cycles in 1 irl day cycle
|
# x in game day cycles in 1 irl day cycle
|
||||||
# time will no longer be in sync
|
# time will no longer be in sync
|
||||||
# can be decimal
|
# can be decimal
|
||||||
modifier: 1.0
|
scale: 1.0
|
|
@ -20,6 +20,10 @@ commands:
|
||||||
permission: realweather.geo
|
permission: realweather.geo
|
||||||
permission-message: You do not have permission to use this command.
|
permission-message: You do not have permission to use this command.
|
||||||
# usage is processed in code
|
# usage is processed in code
|
||||||
|
localtime:
|
||||||
|
description: Get real time in current location
|
||||||
|
permission: realweather.localtime
|
||||||
|
permission-message: You do not have permission to use this command.
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
realweather.command:
|
realweather.command:
|
||||||
|
@ -39,13 +43,16 @@ permissions:
|
||||||
realweather.geo.tp:
|
realweather.geo.tp:
|
||||||
description: Allows teleportation using /geo
|
description: Allows teleportation using /geo
|
||||||
|
|
||||||
|
realweather.localtime:
|
||||||
|
description: Allows /localtime
|
||||||
|
default: true
|
||||||
|
|
||||||
realweather.dynamic:
|
realweather.dynamic:
|
||||||
description: Includes player in dynamic conditions
|
description: Includes player in dynamic conditions
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
realweather.actionbar:
|
realweather.actionbar:
|
||||||
description: Displays status on player's action bar
|
description: Displays status on player's action bar
|
||||||
default: op
|
|
||||||
|
|
||||||
realweather.update.notify:
|
realweather.update.notify:
|
||||||
description: Receive notifications for RealWeather updates
|
description: Receive notifications for RealWeather updates
|
Loading…
Reference in a new issue