/* * 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.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.flowpowered.math.vector.Vector3d; import com.flowpowered.math.vector.Vector3i; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.lanternpowered.server.config.world.WorldConfig; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSetDifficulty; import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutWorldBorder; import org.lanternpowered.server.world.difficulty.LanternDifficulty; import org.lanternpowered.server.world.dimension.LanternDimensionType; import org.lanternpowered.server.world.gen.LanternGeneratorType; import org.lanternpowered.server.world.portal.LanternPortalAgentType; import org.lanternpowered.server.world.rules.Rule; import org.lanternpowered.server.world.rules.RuleDataTypes; import org.lanternpowered.server.world.rules.RuleType; import org.lanternpowered.server.world.rules.Rules; import org.lanternpowered.server.world.weather.LanternWeather; import org.lanternpowered.server.world.weather.WeatherOptions; import org.spongepowered.api.CatalogType; import org.spongepowered.api.GameRegistry; import org.spongepowered.api.Sponge; import org.spongepowered.api.data.DataContainer; import org.spongepowered.api.data.DataQuery; import org.spongepowered.api.data.DataView; import org.spongepowered.api.data.MemoryDataContainer; import org.spongepowered.api.entity.living.player.gamemode.GameMode; import org.spongepowered.api.world.DimensionType; import org.spongepowered.api.world.DimensionTypes; import org.spongepowered.api.world.GeneratorType; import org.spongepowered.api.world.GeneratorTypes; import org.spongepowered.api.world.PortalAgentType; import org.spongepowered.api.world.PortalAgentTypes; import org.spongepowered.api.world.SerializationBehavior; import org.spongepowered.api.world.SerializationBehaviors; import org.spongepowered.api.world.difficulty.Difficulty; import org.spongepowered.api.world.gen.WorldGeneratorModifier; import org.spongepowered.api.world.storage.WorldProperties; import org.spongepowered.api.world.weather.Weather; import org.spongepowered.api.world.weather.Weathers; import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; import javax.annotation.Nullable; public final class LanternWorldProperties implements WorldProperties { private static final int BOUNDARY = 29999984; // The unique id of the world final UUID uniqueId; // The world config WorldConfig worldConfig; // The rules of the world private final Rules rules = new Rules(this); private final TrackerIdAllocator trackerIdAllocator = new TrackerIdAllocator(); // The serialization behavior SerializationBehavior serializationBehavior = SerializationBehaviors.AUTOMATIC; // The extra properties private DataContainer additionalProperties = new MemoryDataContainer(); // The type of the dimension private LanternDimensionType<?> dimensionType = (LanternDimensionType<?>) DimensionTypes.OVERWORLD; // The portal agent type private LanternPortalAgentType portalAgentType = (LanternPortalAgentType) PortalAgentTypes.DEFAULT; // The world generator modifiers ImmutableSet<WorldGeneratorModifier> generatorModifiers = ImmutableSet.of(); // Whether the difficulty is locked private boolean difficultyLocked; // The name of the world private String name; // The spawn position private Vector3i spawnPosition = Vector3i.ZERO; // Whether the world is initialized private boolean initialized; private boolean generateBonusChest; private boolean commandsAllowed; boolean mapFeatures; private final TimeData timeData = new TimeData(); private final WeatherData weatherData = new WeatherData(); @Nullable private LanternWorld world; // World border properties double borderCenterX; double borderCenterZ; // The current radius of the border double borderDiameterStart = 60000000f; double borderDiameterEnd = this.borderDiameterStart; int borderWarningDistance = 5; int borderWarningTime = 15; double borderDamage = 1; double borderDamageThreshold = 5; // The remaining time will be stored in this // for the first world tick long borderLerpTime; // Shrink or growing times private long borderTimeStart = -1; private long borderTimeEnd; // The last time the world was played in private long lastPlayed; public LanternWorldProperties(String name, WorldConfig worldConfig) { this(UUID.randomUUID(), name, worldConfig); } public LanternWorldProperties(UUID uniqueId, String name, WorldConfig worldConfig) { this.worldConfig = worldConfig; this.uniqueId = uniqueId; this.name = name; } public void loadConfig() throws IOException { this.worldConfig.load(); updateWorldGenModifiers(this.worldConfig.getGeneration().getGenerationModifiers()); } public void update(LanternWorldArchetype worldArchetype) throws IOException { this.commandsAllowed = worldArchetype.areCommandsAllowed(); this.dimensionType = worldArchetype.getDimensionType(); this.portalAgentType = worldArchetype.getPortalAgentType(); this.setGeneratorType(worldArchetype.getGeneratorType()); this.worldConfig.getGeneration().setGeneratorSettings(worldArchetype.getGeneratorSettings()); this.generateBonusChest = worldArchetype.doesGenerateBonusChest(); this.mapFeatures = worldArchetype.usesMapFeatures(); this.worldConfig.getGeneration().setSeed(worldArchetype.getSeed()); this.worldConfig.setGameMode(worldArchetype.getGameMode()); this.worldConfig.setAllowPlayerRespawns(worldArchetype.allowPlayerRespawns()); this.worldConfig.setDifficulty(worldArchetype.getDifficulty()); this.worldConfig.setKeepSpawnLoaded(worldArchetype.doesKeepSpawnLoaded()); this.worldConfig.setDoesWaterEvaporate(worldArchetype.waterEvaporates()); this.setGeneratorModifiers(worldArchetype.getGeneratorModifiers()); this.setEnabled(worldArchetype.isEnabled()); this.worldConfig.setPVPEnabled(worldArchetype.isPVPEnabled()); this.setBuildHeight(worldArchetype.getBuildHeight()); this.worldConfig.setHardcore(worldArchetype.isHardcore()); this.worldConfig.setLowHorizon(worldArchetype.getGeneratorType() == GeneratorTypes.FLAT); this.worldConfig.save(); } public void updateWorldGenModifiers(List<String> modifiers) { final ImmutableSet.Builder<WorldGeneratorModifier> genModifiers = ImmutableSet.builder(); final GameRegistry registry = Sponge.getRegistry(); for (String modifier : modifiers) { Optional<WorldGeneratorModifier> genModifier = registry.getType(WorldGeneratorModifier.class, modifier); if (genModifier.isPresent()) { genModifiers.add(genModifier.get()); } else { Lantern.getLogger().error("World generator modifier with id " + modifier + " not found. Missing plugin?"); } } this.generatorModifiers = genModifiers.build(); } /** * Sets the name of the world. * * @param name The name */ public void setName(String name) { this.name = checkNotNull(name, "name"); } /** * Gets the {@link Rules} that is attached to this properties. * * @return the rules */ public Rules getRules() { return this.rules; } public WorldConfig getConfig() { return this.worldConfig; } public boolean doesWaterEvaporate() { return this.worldConfig.doesWaterEvaporate(); } public void setWaterEvaporates(boolean evaporates) { this.worldConfig.setDoesWaterEvaporate(evaporates); } public boolean allowsPlayerRespawns() { return this.worldConfig.allowPlayerRespawns(); } public void setAllowsPlayerRespawns(boolean allow) { boolean update = this.worldConfig.allowPlayerRespawns() != allow; this.worldConfig.setAllowPlayerRespawns(allow); if (update && this.world != null) { this.world.enableSpawnArea(allow); } } public int getBuildHeight() { return this.worldConfig.getMaxBuildHeight(); } public void setBuildHeight(int buildHeight) { this.worldConfig.setMaxBuildHeight(buildHeight); } long getLastPlayedTime() { if (this.world != null) { return this.lastPlayed = System.currentTimeMillis(); } return this.lastPlayed; } void setLastPlayedTime(long time) { this.lastPlayed = time; } public Optional<LanternWorld> getWorld() { return Optional.ofNullable(this.world); } void setWorld(@Nullable LanternWorld world) { this.world = world; if (this.world != null && world == null) { this.lastPlayed = System.currentTimeMillis(); } } @Override public int getContentVersion() { return 0; } @Override public DataContainer toContainer() { // TODO Auto-generated method stub return null; } @Override public boolean isEnabled() { return this.worldConfig.isWorldEnabled(); } @Override public void setEnabled(boolean state) { this.worldConfig.setWorldEnabled(state); } @Override public boolean loadOnStartup() { return this.worldConfig.loadOnStartup(); } @Override public void setLoadOnStartup(boolean state) { this.worldConfig.setLoadOnStartup(state); } @Override public boolean doesKeepSpawnLoaded() { return this.worldConfig.getKeepSpawnLoaded(); } @Override public void setKeepSpawnLoaded(boolean state) { this.worldConfig.setKeepSpawnLoaded(state); } @Override public boolean doesGenerateSpawnOnLoad() { return this.worldConfig.getGeneration().doesGenerateSpawnOnLoad(); } @Override public void setGenerateSpawnOnLoad(boolean state) { this.worldConfig.getGeneration().setGenerateSpawnOnLoad(state); } @Override public String getWorldName() { return this.name; } @Override public UUID getUniqueId() { return this.uniqueId; } @Override public Vector3i getSpawnPosition() { return this.spawnPosition; } @Override public void setSpawnPosition(Vector3i position) { this.spawnPosition = checkNotNull(position, "position"); // Generate the spawn are at the new spawn position // if the world is present boolean keepSpawnLoaded = this.worldConfig.getKeepSpawnLoaded(); if (keepSpawnLoaded && this.world != null) { this.world.enableSpawnArea(true); } } @Override public LanternGeneratorType getGeneratorType() { return (LanternGeneratorType) this.worldConfig.getGeneration().getGeneratorType(); } @Override public void setGeneratorType(GeneratorType generatorType) { this.worldConfig.getGeneration().setGeneratorType(generatorType); } @Override public DataContainer getGeneratorSettings() { return this.worldConfig.getGeneration().getGeneratorSettings(); } @Override public long getSeed() { return this.worldConfig.getGeneration().getSeed(); } @Override public long getTotalTime() { return this.timeData.getAge(); } @Override public long getWorldTime() { return this.timeData.getDayTime(); } @Override public void setWorldTime(long time) { this.timeData.setDayTime(time); } @Override public LanternDimensionType getDimensionType() { return this.dimensionType; } @Override public LanternPortalAgentType getPortalAgentType() { return this.portalAgentType; } /** * Sets the {@link DimensionType}. * * @param dimensionType The dimension type */ public void setDimensionType(DimensionType dimensionType) { this.dimensionType = (LanternDimensionType<?>) checkNotNull(dimensionType, "dimensionType"); } public void setPortalAgentType(PortalAgentType portalAgentType) { this.portalAgentType = (LanternPortalAgentType) checkNotNull(portalAgentType, "portalAgentType"); } @Override public boolean isRaining() { final Weather weather = this.weatherData.getWeather(); return ((LanternWeather) weather).getOptions().getOrDefault(WeatherOptions.RAIN_STRENGTH).get() > 0; } @Override public void setRaining(boolean state) { LanternWeather weather = this.weatherData.getWeather(); final boolean raining = weather.getOptions().getOrDefault(WeatherOptions.RAIN_STRENGTH).get() > 0; if (raining != state) { weather = (LanternWeather) (state ? Weathers.RAIN : Weathers.CLEAR); this.weatherData.setWeather(weather); this.weatherData.setRemainingDuration(weather.getRandomTicksDuration()); this.weatherData.setRunningDuration(0); } } @Override public int getRainTime() { return this.isRaining() ? (int) this.weatherData.getRemainingDuration() : 0; } @Override public void setRainTime(int time) { final Weather weather = this.weatherData.getWeather(); final boolean raining = ((LanternWeather) weather).getOptions().getOrDefault(WeatherOptions.RAIN_STRENGTH).get() > 0; if (raining) { this.weatherData.setRemainingDuration(time); } } @Override public boolean isThundering() { final Weather weather = this.weatherData.getWeather(); return weather == Weathers.THUNDER_STORM; } @Override public void setThundering(boolean state) { LanternWeather weather = this.weatherData.getWeather(); final boolean thunderStorm = weather == Weathers.THUNDER_STORM; if (thunderStorm != state) { weather = (LanternWeather) (state ? Weathers.THUNDER_STORM : Weathers.CLEAR); this.weatherData.setWeather(weather); this.weatherData.setRemainingDuration(weather.getRandomTicksDuration()); this.weatherData.setRunningDuration(0); } } @Override public int getThunderTime() { return this.isThundering() ? (int) this.weatherData.getRemainingDuration() : 0; } @Override public void setThunderTime(int time) { final Weather weather = this.weatherData.getWeather(); final boolean thunderStorm = weather == Weathers.THUNDER_STORM; if (thunderStorm) { this.weatherData.setRemainingDuration(time); } } @Override public GameMode getGameMode() { return this.worldConfig.getGameMode(); } @Override public void setGameMode(GameMode gameMode) { this.worldConfig.setGameMode(gameMode); } @Override public boolean usesMapFeatures() { return this.mapFeatures; } @Override public void setMapFeaturesEnabled(boolean state) { this.mapFeatures = state; } @Override public boolean isHardcore() { return this.worldConfig.isHardcore(); } @Override public void setHardcore(boolean state) { this.worldConfig.setHardcore(state); } @Override public boolean areCommandsAllowed() { return this.commandsAllowed; } @Override public void setCommandsAllowed(boolean state) { this.commandsAllowed = state; } @Override public boolean isInitialized() { return this.initialized; } /** * Sets whether the world properties and world are initialized. * * @param initialized Is initialized */ public void setInitialized(boolean initialized) { this.initialized = initialized; } @Override public Difficulty getDifficulty() { return this.worldConfig.getDifficulty(); } @Override public void setDifficulty(Difficulty difficulty) { checkNotNull(difficulty, "difficulty"); if (this.getDifficulty() != difficulty && this.world != null) { this.world.broadcast(() -> new MessagePlayOutSetDifficulty((LanternDifficulty) difficulty)); } this.worldConfig.setDifficulty(difficulty); } @Override public boolean doesGenerateBonusChest() { return this.generateBonusChest; } public void setGenerateBonusChest(boolean generateBonusChest) { this.generateBonusChest = generateBonusChest; } @Override public Optional<String> getGameRule(String gameRule) { final Optional<RuleType<?>> optRuleType = RuleType.get(gameRule); if (!optRuleType.isPresent()) { return Optional.empty(); } //noinspection unchecked final Optional<Rule> rule = this.rules.getRule((RuleType) optRuleType.get()); if (!rule.isPresent()) { return Optional.empty(); } return Optional.of(rule.get().getRawValue()); } @Override public Map<String, String> getGameRules() { final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); for (Map.Entry<RuleType<?>, Rule<?>> entry : this.rules.getRules().entrySet()) { builder.put(entry.getKey().getName(), entry.getValue().getRawValue()); } return builder.build(); } @Override public void setGameRule(String gameRule, String value) { // We cannot know what type a plugin rule would be, so string this.rules.getOrCreateRule(RuleType.getOrCreate(gameRule, RuleDataTypes.STRING, "")).setRawValue(value); } @Override public DataContainer getAdditionalProperties() { return this.additionalProperties; } @Override public Optional<DataView> getPropertySection(DataQuery path) { return this.additionalProperties.getView(path); } @Override public void setPropertySection(DataQuery path, DataView data) { this.additionalProperties.set(path, data); } /** * Sets the additional properties {@link DataContainer}. * * @param additionalProperties The additional properties */ public void setAdditionalProperties(DataContainer additionalProperties) { this.additionalProperties = checkNotNull(additionalProperties, "additionalProperties"); } @Override public Collection<WorldGeneratorModifier> getGeneratorModifiers() { return this.generatorModifiers; } @Override public void setGeneratorModifiers(Collection<WorldGeneratorModifier> modifiers) { this.generatorModifiers = ImmutableSet.copyOf(this.generatorModifiers); final List<String> genModifiers = this.worldConfig.getGeneration().getGenerationModifiers(); genModifiers.clear(); genModifiers.addAll(modifiers.stream().map(CatalogType::getId).collect(Collectors.toList())); } @Override public SerializationBehavior getSerializationBehavior() { return this.serializationBehavior; } @Override public void setSerializationBehavior(SerializationBehavior behavior) { this.serializationBehavior = checkNotNull(behavior, "behavior"); } @Override public Vector3d getWorldBorderCenter() { return new Vector3d(this.borderCenterX, 0, this.borderCenterZ); } public MessagePlayOutWorldBorder createWorldBorderMessage() { return new MessagePlayOutWorldBorder.Initialize(this.borderCenterX, this.borderCenterZ, this.borderDiameterStart, this.borderDiameterEnd, this.getWorldBorderTimeRemaining(), BOUNDARY, this.borderWarningDistance, this.borderWarningTime); } public void setBorderDiameter(double startDiameter, double endDiameter, long time) { checkArgument(startDiameter >= 0, "The start diameter cannot be negative!"); checkArgument(endDiameter >= 0, "The end diameter cannot be negative!"); checkArgument(time >= 0, "The duration cannot be negative!"); // Only shrink or grow if needed if (time == 0 || startDiameter == endDiameter) { this.borderDiameterStart = endDiameter; this.borderDiameterEnd = endDiameter; this.setCurrentBorderTime(0); if (this.world != null) { this.world.broadcast(() -> new MessagePlayOutWorldBorder.UpdateDiameter(endDiameter)); } } else { this.borderDiameterStart = startDiameter; this.borderDiameterEnd = endDiameter; this.setCurrentBorderTime(time); if (this.world != null) { this.world.broadcast(() -> new MessagePlayOutWorldBorder.UpdateLerpedDiameter(startDiameter, endDiameter, time)); } } } @Override public void setWorldBorderCenter(double x, double z) { this.borderCenterX = x; this.borderCenterZ = z; if (this.world != null) { this.world.broadcast(() -> new MessagePlayOutWorldBorder.UpdateCenter(this.borderCenterX, this.borderCenterZ)); } } @Override public double getWorldBorderDiameter() { if (this.borderTimeStart == -1) { this.updateCurrentBorderTime(); } if (this.borderDiameterStart != this.borderDiameterEnd) { long lerpTime = this.borderTimeEnd - this.borderTimeStart; if (lerpTime == 0) { return this.borderDiameterStart; } long elapsedTime = System.currentTimeMillis() - this.borderTimeStart; elapsedTime = elapsedTime > lerpTime ? lerpTime : elapsedTime < 0 ? 0 : elapsedTime; double d = elapsedTime / lerpTime; double diameter; if (d == 0.0) { diameter = this.borderDiameterStart; } else { diameter = this.borderDiameterStart + (this.borderDiameterEnd - this.borderDiameterStart) * d; } this.borderDiameterStart = diameter; this.setCurrentBorderTime(lerpTime - elapsedTime); return diameter; } else { return this.borderDiameterStart; } } @Override public void setWorldBorderDiameter(double diameter) { this.borderDiameterStart = diameter; } @Override public long getWorldBorderTimeRemaining() { if (this.borderTimeStart == -1) { this.updateCurrentBorderTime(); } return Math.max(this.borderTimeEnd - System.currentTimeMillis(), 0); } void updateCurrentBorderTime() { this.updateCurrentBorderTime(this.borderLerpTime); } private void setCurrentBorderTime(long time) { this.updateCurrentBorderTime(time); this.borderLerpTime = time; } private void updateCurrentBorderTime(long time) { this.borderTimeStart = System.currentTimeMillis(); this.borderTimeEnd = this.borderTimeStart + time; } @Override public void setWorldBorderTimeRemaining(long time) { this.setCurrentBorderTime(time); if (this.world != null) { this.world.broadcast(() -> time == 0 ? new MessagePlayOutWorldBorder.UpdateDiameter(this.borderDiameterEnd) : new MessagePlayOutWorldBorder.UpdateLerpedDiameter(this.getWorldBorderDiameter(), this.borderDiameterEnd, this.getWorldBorderTimeRemaining())); } } @Override public double getWorldBorderTargetDiameter() { return this.borderDiameterEnd; } @Override public void setWorldBorderTargetDiameter(double diameter) { this.borderDiameterEnd = diameter; if (this.world != null) { this.world.broadcast(() -> this.getWorldBorderTimeRemaining() == 0 ? new MessagePlayOutWorldBorder.UpdateDiameter( diameter) : new MessagePlayOutWorldBorder.UpdateLerpedDiameter(this.getWorldBorderDiameter(), diameter, this.getWorldBorderTimeRemaining())); } } @Override public double getWorldBorderDamageThreshold() { return this.borderDamageThreshold; } @Override public void setWorldBorderDamageThreshold(double distance) { this.borderDamageThreshold = distance; } @Override public double getWorldBorderDamageAmount() { return this.borderDamage; } @Override public void setWorldBorderDamageAmount(double damage) { this.borderDamage = damage; } @Override public int getWorldBorderWarningTime() { return this.borderWarningTime; } @Override public void setWorldBorderWarningTime(int time) { this.borderWarningTime = time; if (this.world != null) { this.world.broadcast(() -> new MessagePlayOutWorldBorder.UpdateWarningTime(time)); } } @Override public int getWorldBorderWarningDistance() { return this.borderWarningDistance; } @Override public void setWorldBorderWarningDistance(int distance) { this.borderWarningDistance = distance; if (this.world != null) { this.world.broadcast(() -> new MessagePlayOutWorldBorder.UpdateWarningDistance(distance)); } } @Override public boolean isPVPEnabled() { return this.worldConfig.getPVPEnabled(); } @Override public void setPVPEnabled(boolean enabled) { this.worldConfig.setPVPEnabled(enabled); } /** * Gets whether the {@link Difficulty} is locked. This is only used in singleplayer. * * @return Is difficulty locked */ public boolean isDifficultyLocked() { return this.difficultyLocked; } /** * Sets whether the {@link Difficulty} is locked. This is only used in singleplayer. * * @param difficultyLocked Is difficulty locked */ public void setDifficultyLocked(boolean difficultyLocked) { this.difficultyLocked = difficultyLocked; } public WeatherData getWeatherData() { return this.weatherData; } public TimeData getTimeData() { return this.timeData; } public TrackerIdAllocator getTrackerIdAllocator() { return this.trackerIdAllocator; } }