/*
* CraftBook Copyright (C) 2010-2017 sk89q <http://www.sk89q.com>
* CraftBook Copyright (C) 2011-2017 me4502 <http://www.me4502.com>
* CraftBook Copyright (C) Contributors
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not,
* see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.craftbook.sponge.mechanics;
import com.google.inject.Inject;
import com.me4502.modularframework.module.Module;
import com.me4502.modularframework.module.guice.ModuleConfiguration;
import com.sk89q.craftbook.core.util.ConfigValue;
import com.sk89q.craftbook.core.util.CraftBookException;
import com.sk89q.craftbook.core.util.documentation.DocumentationProvider;
import com.sk89q.craftbook.sponge.CraftBookPlugin;
import com.sk89q.craftbook.sponge.mechanics.types.SpongeMechanic;
import ninja.leaping.configurate.ConfigurationNode;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.block.BlockTypes;
import org.spongepowered.api.data.key.Keys;
import org.spongepowered.api.data.manipulator.mutable.block.LayeredData;
import org.spongepowered.api.data.property.block.ReplaceableProperty;
import org.spongepowered.api.data.property.block.TemperatureProperty;
import org.spongepowered.api.data.value.mutable.MutableBoundedValue;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.block.TickBlockEvent;
import org.spongepowered.api.event.cause.Cause;
import org.spongepowered.api.event.cause.NamedCause;
import org.spongepowered.api.util.Direction;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
import org.spongepowered.api.world.weather.Weathers;
import java.util.Optional;
@Module(id = "snow", name = "Snow", onEnable="onInitialize", onDisable="onDisable")
public class Snow extends SpongeMechanic implements DocumentationProvider {
/**
* An array of directions that snow can move in. In order of preference.
*/
private static final Direction[] VALID_SNOW_DIRECTIONS = {Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST};
@Inject
@ModuleConfiguration
public ConfigurationNode config;
private ConfigValue<Boolean> dispersionMode = new ConfigValue<>("dispersion-mode", "A realistic version of snow that disperses as it piles.", false);
private ConfigValue<Boolean> highPiling = new ConfigValue<>("high-piling", "Allows snow to pile up multiple blocks.", false);
private ConfigValue<Boolean> waterFreezing = new ConfigValue<>("water-freezing", "Allows snow to freeze water beneath it.", false);
private ConfigValue<Boolean> meltInSunlight = new ConfigValue<>("melt-in-sunlight", "Allows snow to melt when in sunlight.", false);
private ConfigValue<Boolean> partialMeltOnly = new ConfigValue<>("partial-melt-only", "When `melt-in-sunlight` is enabled, only melt to vanilla height.", false);
@Override
public void onInitialize() throws CraftBookException {
super.onInitialize();
//Give the snow blocks the ability to tick randomly.
BlockTypes.SNOW_LAYER.setTickRandomly(true);
BlockTypes.SNOW.setTickRandomly(true);
dispersionMode.load(config);
highPiling.load(config);
waterFreezing.load(config);
meltInSunlight.load(config);
partialMeltOnly.load(config);
}
@Listener
public void onBlockTick(TickBlockEvent event) {
event.getTargetBlock().getLocation().ifPresent(location -> {
if (isSnowBlock(location)) {
if (getTemperature(location) == TemperatureType.FREEZING) {
//Only increase snow at valid blocks, where snow could actually fall.
if (location.getBlockType() == BlockTypes.SNOW_LAYER && canSnowReach(location))
increaseSnow(location, true);
} else if (meltInSunlight.getValue() && !isBlockBuried(location) && getTemperature(location) == TemperatureType.WARM) {
//Only melt if on top, and too hot.
if(partialMeltOnly.getValue() && location.get(Keys.LAYER).orElse(0) == 1) {
return;
}
//Lower the snow
decreaseSnow(location);
} else if (!isBlockBuried(location)) {
disperseSnow(location, null);
}
}
});
}
private void increaseSnow(Location<?> location, boolean disperse) {
Optional<MutableBoundedValue<Integer>> optionalHeightValue = location.getValue(Keys.LAYER);
if(location.getBlockType() == BlockTypes.SNOW_LAYER && optionalHeightValue.isPresent()) {
MutableBoundedValue<Integer> heightValue = optionalHeightValue.get();
int newHeight = heightValue.get() + 1;
if(newHeight > heightValue.getMaxValue()) {
if(highPiling.getValue())
location.setBlockType(BlockTypes.SNOW, Cause.of(NamedCause.source(CraftBookPlugin.spongeInst().getContainer())));
} else {
location.offer(Keys.LAYER, newHeight);
if(disperse)
disperseSnow(location, null);
}
if(waterFreezing.getValue()) {
Location down = location.getRelative(Direction.DOWN);
if (down.getBlockType() == BlockTypes.WATER || down.getBlockType() == BlockTypes.FLOWING_WATER)
down.setBlockType(BlockTypes.ICE, Cause.of(NamedCause.source(CraftBookPlugin.spongeInst().getContainer())));
}
} else {
location.setBlockType(BlockTypes.SNOW_LAYER, Cause.of(NamedCause.source(CraftBookPlugin.spongeInst().getContainer())));
}
}
private static void decreaseSnow(Location<?> location) {
Optional<MutableBoundedValue<Integer>> optionalHeightValue = location.getValue(Keys.LAYER);
if(optionalHeightValue.isPresent()) {
MutableBoundedValue<Integer> heightValue = optionalHeightValue.get();
int newHeight = heightValue.get() - 1;
if(newHeight < heightValue.getMinValue())
location.setBlockType(BlockTypes.AIR, Cause.of(NamedCause.source(CraftBookPlugin.spongeInst().getContainer())));
else
location.offer(Keys.LAYER, newHeight);
} else if (location.getBlockType() == BlockTypes.SNOW) {
location.setBlockType(BlockTypes.SNOW_LAYER, Cause.of(NamedCause.source(CraftBookPlugin.spongeInst().getContainer())));
LayeredData data = location.getOrCreate(LayeredData.class).get();
data.set(Keys.LAYER, data.getValue(Keys.LAYER).get().getMaxValue());
location.offer(data);
}
}
private void disperseSnow(final Location<?> location, Direction ignoredFace) {
if(!dispersionMode.getValue())
return;
int currentHeight = location.get(Keys.LAYER).orElse(0);
if(currentHeight == 0)
return;
Direction currentSmallest = null;
int currentSmallestInt = Integer.MAX_VALUE;
for(final Direction dir : VALID_SNOW_DIRECTIONS) {
if(dir == ignoredFace || (currentHeight == 1 && dir != Direction.DOWN)) continue;
final Location<?> relative = location.getRelative(dir);
if(canPlaceSnowAt(relative)) {
int otherHeight = relative.get(Keys.LAYER).orElse(0);
if(otherHeight >= 1) {
if(dir != Direction.DOWN && currentHeight < otherHeight+2) {
continue;
}
}
if(dir == Direction.DOWN) {
increaseSnow(relative, false);
decreaseSnow(location);
Sponge.getGame().getScheduler().createTaskBuilder().delayTicks(20L).execute(() -> {
disperseSnow(relative, dir.getOpposite());
if (isBlockBuried(location))
disperseSnow(location.getRelative(Direction.UP), Direction.NONE);
}).submit(CraftBookPlugin.inst());
break;
} else if(currentSmallest == null || currentSmallestInt > otherHeight) {
currentSmallest = dir;
currentSmallestInt = otherHeight;
if(currentSmallestInt == 0)
break; //Can't get smaller.
}
}
}
if(currentSmallest != null) {
Location<?> relative = location.getRelative(currentSmallest);
increaseSnow(relative, false);
decreaseSnow(location);
Direction finalCurrentSmallest = currentSmallest;
Sponge.getGame().getScheduler().createTaskBuilder().delayTicks(20L).execute(() -> {
disperseSnow(relative, finalCurrentSmallest.getOpposite());
if (isBlockBuried(location))
disperseSnow(location.getRelative(Direction.UP), Direction.NONE);
}).submit(CraftBookPlugin.inst());
}
}
/**
* Determines whether or not snow is able to reach this block.
*
* @param location The block.
* @return If snow can reach it.
*/
private static boolean canSnowReach(Location<?> location) {
while(location.getBlockY() < location.getExtent().getBlockMax().getY()) {
location = location.getRelative(Direction.UP);
if(location.getBlockType() != BlockTypes.AIR
&& location.getBlockType() != BlockTypes.LEAVES
&& location.getBlockType() != BlockTypes.LEAVES2)
return false;
}
return true;
}
/**
* Gets whether the block is replacable with snow.
*
* @param location The block.
* @return If it can be replaced with snow.
*/
private static boolean canPlaceSnowAt(Location<?> location) {
if (location.getBlockType() == BlockTypes.SNOW_LAYER || location.getBlockType() == BlockTypes.AIR)
return true;
ReplaceableProperty replaceableProperty = location.getBlockType().getProperty(ReplaceableProperty.class).orElse(null);
return replaceableProperty != null
&& !(location.getBlockType() == BlockTypes.WATER
|| location.getBlockType() == BlockTypes.FLOWING_WATER
|| location.getBlockType() == BlockTypes.LAVA
|| location.getBlockType() == BlockTypes.FLOWING_LAVA)
&& replaceableProperty.getValue() != null
&& replaceableProperty.getValue();
}
/**
* Determines if the specified block is buried under snow.
*
* @param location The location of the block.
* @return If it is buried.
*/
private static boolean isBlockBuried(Location<?> location) {
return isSnowBlock(location.getRelative(Direction.UP));
}
/**
* Checks whether the block is snow.
*
* @param location The location of the block.
* @return If it's snow
*/
private static boolean isSnowBlock(Location<?> location) {
return location.getBlockType() == BlockTypes.SNOW || location.getBlockType() == BlockTypes.SNOW_LAYER;
}
/**
* Gets the temperature of the block.
*
* @param location The location of the block.
* @return The temperature
*/
private static TemperatureType getTemperature(Location<World> location) {
Optional<TemperatureProperty> temperaturePropertyOptional = location.getProperty(TemperatureProperty.class);
if(!temperaturePropertyOptional.isPresent())
return TemperatureType.UNKNOWN;
Double temperature = temperaturePropertyOptional.get().getValue();
if(temperature == null)
return TemperatureType.UNKNOWN;
if(temperature < 0.15 && location.getExtent().getWeather() == Weathers.RAIN)
return TemperatureType.FREEZING;
else if (temperature < 0.15)
return TemperatureType.COLD;
else
return TemperatureType.WARM;
}
private enum TemperatureType {
FREEZING, //Snow generates
COLD, //Snow survives
WARM, //Snow melts
UNKNOWN //Unknown - don't do anything
}
@Override
public String getName() {
return "BetterSnow";
}
@Override
public String getPath() {
return "mechanics/snow";
}
@Override
public ConfigValue<?>[] getConfigurationNodes() {
return new ConfigValue<?>[]{
dispersionMode,
highPiling,
waterFreezing,
meltInSunlight,
partialMeltOnly
};
}
}