/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.world;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableMap;
import org.lanternpowered.api.script.ScriptContext;
import org.lanternpowered.api.script.context.Parameters;
import org.lanternpowered.api.world.weather.WeatherUniverse;
import org.lanternpowered.server.component.AttachableTo;
import org.lanternpowered.server.component.Component;
import org.lanternpowered.server.component.Locked;
import org.lanternpowered.server.component.OnAttach;
import org.lanternpowered.server.inject.Inject;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutWorldSky;
import org.lanternpowered.server.script.context.ContextImpl;
import org.lanternpowered.server.world.rules.RuleTypes;
import org.lanternpowered.server.world.weather.LanternWeather;
import org.lanternpowered.server.world.weather.WeatherOptions;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.event.SpongeEventFactory;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.event.world.ChangeWorldWeatherEvent;
import org.spongepowered.api.util.weighted.WeightedTable;
import org.spongepowered.api.world.weather.Weather;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@Locked
@AttachableTo(LanternWorld.class)
public final class LanternWeatherUniverse implements Component, WeatherUniverse {
private static final float FADE_SPEED = 0.01f;
// The random used for the random weather times
private final Random random = new Random();
// The world this weather universe is attached to
@Inject private LanternWorld world;
private WeatherData weatherData;
private float rainStrengthTarget;
private float rainStrength;
private float darknessTarget;
private float darkness;
private ScriptContext context;
@OnAttach
private void onAttach() {
this.weatherData = this.world.getProperties().getWeatherData();
this.rainStrengthTarget = this.weatherData.getWeather().getOptions().getOrDefault(WeatherOptions.RAIN_STRENGTH).get().floatValue();
this.rainStrength = this.rainStrengthTarget;
this.darknessTarget = this.weatherData.getWeather().getOptions().getOrDefault(WeatherOptions.SKY_DARKNESS).get().floatValue();
this.darkness = this.darknessTarget;
this.context = new ContextImpl(ImmutableMap.of(Parameters.WORLD, this.world));
}
/**
* Pulses the weather.
*/
void pulse() {
this.pulseWeather();
this.pulseSky();
}
private void pulseWeather() {
if (!this.world.getOrCreateRule(RuleTypes.DO_WEATHER_CYCLE).getValue()) {
return;
}
this.weatherData.setRunningDuration(this.weatherData.getRunningDuration() + 1);
final long remaining = this.weatherData.getRemainingDuration() - 1;
if (remaining <= 0) {
final LanternWeather next = this.nextWeather();
if (this.setWeather(next, next.getRandomTicksDuration(), true)) {
// If the event is cancelled, continue the current weather for
// a random amount of time, maybe more luck next time
final LanternWeather current = this.weatherData.getWeather();
this.weatherData.setRemainingDuration(current.getRandomTicksDuration());
}
} else {
this.weatherData.setRemainingDuration(remaining);
}
this.weatherData.getWeather().getAction().run(this.context);
}
private void pulseSky() {
boolean updateSky = false;
if (this.darkness != this.darknessTarget) {
if (Math.abs(this.darkness - this.darknessTarget) < FADE_SPEED) {
this.darkness = this.darknessTarget;
} else if (this.darkness > this.darknessTarget) {
this.darkness -= FADE_SPEED;
} else {
this.darkness += FADE_SPEED;
}
updateSky = true;
}
if (this.rainStrength != this.rainStrengthTarget) {
if (Math.abs(this.rainStrength - this.rainStrengthTarget) < FADE_SPEED) {
this.rainStrength = this.rainStrengthTarget;
} else if (this.rainStrength > this.rainStrengthTarget) {
this.rainStrength -= FADE_SPEED;
} else {
this.rainStrength += FADE_SPEED;
}
updateSky = true;
}
if (updateSky) {
this.world.broadcast(this::createSkyUpdateMessage);
}
}
/**
* Creates a new {@link MessagePlayOutWorldSky} for the
* current state of the sky.
*
* @return The message
*/
public MessagePlayOutWorldSky createSkyUpdateMessage() {
return new MessagePlayOutWorldSky(this.rainStrength, this.darkness);
}
/**
* Gets the next possible {@link LanternWeather}, ignoring
* the last weather type.
*
* @return The next weather type
*/
private LanternWeather nextWeather() {
//noinspection unchecked
final List<LanternWeather> weathers = new ArrayList(this.world.game.getRegistry().getAllOf(Weather.class));
final LanternWeather current = this.weatherData.getWeather();
weathers.remove(current);
if (weathers.isEmpty()) {
return current;
}
final WeightedTable<LanternWeather> table = new WeightedTable<>();
weathers.forEach(weather -> table.add(weather, weather.getWeight()));
return table.get(this.random).get(0);
}
@Override
public LanternWeather getWeather() {
return this.weatherData.getWeather();
}
@Override
public long getRemainingDuration() {
return this.weatherData.getRemainingDuration();
}
@Override
public long getRunningDuration() {
return this.weatherData.getRunningDuration();
}
@Override
public void setWeather(Weather weather) {
this.setWeather(weather, ((LanternWeather) weather).getRandomTicksDuration());
}
@Override
public void setWeather(Weather weather, long duration) {
this.setWeather(weather, duration, false);
}
private boolean setWeather(Weather weather, long duration, boolean doEvent) {
checkNotNull(weather, "weather");
final Cause cause = Cause.source(this.world).build();
final LanternWeather current = this.weatherData.getWeather();
if (doEvent) {
final ChangeWorldWeatherEvent event = SpongeEventFactory.createChangeWorldWeatherEvent(
cause, (int) duration, (int) duration, current, weather, weather, this.world);
Sponge.getEventManager().post(event);
if (event.isCancelled()) {
return true;
}
weather = event.getWeather();
duration = event.getDuration();
}
final LanternWeather weather0 = (LanternWeather) weather;
this.weatherData.setRemainingDuration(duration);
if (current != weather) {
this.weatherData.setRunningDuration(0);
this.weatherData.setWeather(weather0);
}
this.darknessTarget = weather0.getOptions().getOrDefault(WeatherOptions.SKY_DARKNESS).get().floatValue();
this.rainStrengthTarget = weather0.getOptions().getOrDefault(WeatherOptions.RAIN_STRENGTH).get().floatValue();
return false;
}
@Override
public double getDarkness() {
return this.darkness;
}
}