blob: 40d0dc8e628d4976cf6829e10e8c68a74d76d020 [file] [log] [blame] [raw]
package net.glowstone;
import com.flowpowered.network.Message;
import com.google.common.base.Preconditions;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Level;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.glowstone.block.GlowBlock;
import net.glowstone.block.ItemTable;
import net.glowstone.block.blocktype.BlockType;
import net.glowstone.chunk.ChunkManager;
import net.glowstone.chunk.ChunkManager.ChunkLock;
import net.glowstone.chunk.ChunkSection;
import net.glowstone.chunk.GlowChunk;
import net.glowstone.chunk.GlowChunk.Key;
import net.glowstone.chunk.GlowChunkSnapshot.EmptySnapshot;
import net.glowstone.constants.GlowBiome;
import net.glowstone.constants.GlowBiomeClimate;
import net.glowstone.constants.GlowEffect;
import net.glowstone.constants.GlowParticle;
import net.glowstone.constants.GlowSound;
import net.glowstone.constants.GlowTree;
import net.glowstone.data.CommandFunction;
import net.glowstone.entity.CustomEntityDescriptor;
import net.glowstone.entity.EntityManager;
import net.glowstone.entity.EntityRegistry;
import net.glowstone.entity.GlowEntity;
import net.glowstone.entity.GlowLightningStrike;
import net.glowstone.entity.GlowLivingEntity;
import net.glowstone.entity.GlowPlayer;
import net.glowstone.entity.GlowTntPrimed;
import net.glowstone.entity.objects.GlowFallingBlock;
import net.glowstone.entity.objects.GlowItem;
import net.glowstone.entity.physics.BoundingBox;
import net.glowstone.generator.structures.GlowStructure;
import net.glowstone.io.WorldMetadataService.WorldFinalValues;
import net.glowstone.io.WorldStorageProvider;
import net.glowstone.net.message.play.entity.EntityStatusMessage;
import net.glowstone.net.message.play.game.BlockChangeMessage;
import net.glowstone.net.message.play.player.ServerDifficultyMessage;
import net.glowstone.util.BlockStateDelegate;
import net.glowstone.util.GameRuleManager;
import net.glowstone.util.RayUtil;
import net.glowstone.util.collection.ConcurrentSet;
import net.glowstone.util.config.WorldConfig;
import org.bukkit.BlockChangeDelegate;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Difficulty;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.TreeType;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.FallingBlock;
import org.bukkit.entity.Item;
import org.bukkit.entity.LightningStrike;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.event.weather.LightningStrikeEvent;
import org.bukkit.event.weather.ThunderChangeEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.bukkit.event.world.SpawnChangeEvent;
import org.bukkit.event.world.StructureGrowEvent;
import org.bukkit.event.world.WorldInitEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.inventory.ItemStack;
import org.bukkit.material.MaterialData;
import org.bukkit.metadata.MetadataStore;
import org.bukkit.metadata.MetadataStoreBase;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.messaging.StandardMessenger;
import org.bukkit.util.Consumer;
import org.bukkit.util.Vector;
/**
* A class which represents the in-game world.
*
* @author Graham Edgecombe
*/
@ToString(of = "name")
public final class GlowWorld implements World {
/**
* The length in ticks of one Minecraft day.
*/
public static final long DAY_LENGTH = 24000;
/**
* The metadata store for world objects.
*/
private static final MetadataStore<World> metadata = new WorldMetadataStore();
private static final int TICKS_PER_SECOND = 20;
private static final int HALF_DAY_IN_TICKS = 12000;
private static final int WEEK_IN_TICKS = 14 * HALF_DAY_IN_TICKS;
/**
* The length in ticks between autosaves (5 minutes).
*/
private static final int AUTOSAVE_TIME = TICKS_PER_SECOND * 60 * 5;
/**
* The maximum height of ocean water.
*/
private static int seaLevel;
/**
* The server of this world.
*/
private final GlowServer server;
/**
* The name of this world.
*/
private final String name;
/**
* The chunk manager.
*/
private final ChunkManager chunks;
/**
* The world metadata service used.
*/
private final WorldStorageProvider storageProvider;
/**
* The world's UUID.
*/
private final UUID uid;
/**
* The entity manager.
*/
private final EntityManager entities = new EntityManager();
/**
* The chunk generator for this world.
*/
private final ChunkGenerator generator;
/**
* The world populators for this world.
*/
private final List<BlockPopulator> populators;
/**
* The game rules used in this world.
*/
private final GameRuleManager gameRules = new GameRuleManager();
/**
* The environment.
*/
private final Environment environment;
/**
* Whether structure generation is enabled.
*/
private final boolean generateStructures;
/**
* The world seed.
*/
private final long seed;
/**
* Contains how regular blocks should be pulsed.
*/
private final ConcurrentSet<Location> tickMap = new ConcurrentSet<>();
private final Spigot spigot = new Spigot() {
@Override
public void playEffect(Location location, Effect effect) {
GlowWorld.this.playEffect(location, effect, 0);
}
@Override
public void playEffect(Location location, Effect effect, int id, int data, float offsetX,
float offsetY, float offsetZ, float speed, int particleCount, int radius) {
showParticle(location, effect, id, data, offsetX, offsetY, offsetZ, speed,
particleCount, radius);
}
@Override
public LightningStrike strikeLightning(Location loc, boolean isSilent) {
return strikeLightningFireEvent(loc, false, isSilent);
}
@Override
public LightningStrike strikeLightningEffect(Location loc, boolean isSilent) {
return strikeLightningFireEvent(loc, true, isSilent);
}
};
/*/**
* The ScheduledExecutorService the for entity AI tasks threading.
*/
//private final ScheduledExecutorService aiTaskService;
/**
* The world border.
*/
private final GlowWorldBorder worldBorder;
/**
* The functions for this world.
*/
private final Map<String, CommandFunction> functions;
/**
* A lock kept on the spawn chunks.
*/
private ChunkLock spawnChunkLock;
/**
* The world type.
*/
@Getter
@Setter
private WorldType worldType;
/**
* The spawn position.
*/
private Location spawnLocation;
/**
* Whether to keep the spawn chunks in memory (prevent them from being unloaded).
*/
private boolean keepSpawnLoaded = true;
/**
* Whether to populate chunks when they are anchored.
*/
private boolean populateAnchoredChunks;
/**
* Whether PvP is allowed in this world.
*/
private boolean pvpAllowed = true;
/**
* Whether animals can spawn in this world.
*/
private boolean spawnAnimals = true;
/**
* Whether monsters can spawn in this world.
*/
private boolean spawnMonsters = true;
/**
* Whether it is currently raining/snowing on this world.
*/
private boolean currentlyRaining = true;
/**
* How many ticks until the rain/snow status is expected to change.
*/
private int rainingTicks;
/**
* Whether it is currently thundering on this world.
*/
private boolean currentlyThundering = true;
/**
* How many ticks until the thundering status is expected to change.
*/
private int thunderingTicks;
/**
* The rain density on the current world tick.
*/
private float currentRainDensity;
/**
* The sky darkness on the current world tick.
*/
private float currentSkyDarkness;
/**
* The age of the world, in ticks.
*/
private long worldAge;
/**
* The current world time.
*/
private long time;
/**
* The time until the next full-save.
*/
private int saveTimer = AUTOSAVE_TIME;
/**
* The check to autosave.
*/
private boolean autosave = true;
/**
* The world's gameplay difficulty.
*/
private Difficulty difficulty;
/**
* Ticks between when passive mobs are spawned.
*/
private long ticksPerAnimal;
/**
* Ticks between when hostile mobs are spawned.
*/
private long ticksPerMonster;
/**
* Per-world spawn limits on hostile mobs.
*/
private int monsterLimit;
/**
* Per-world spawn limits on passive mobs.
*/
private int animalLimit;
/**
* Per-world spawn limits on water mobs (squids).
*/
private int waterAnimalLimit;
/**
* Per-world spawn limits on ambient mobs (bats).
*/
private int ambientLimit;
private Map<Integer, GlowStructure> structures;
/**
* The maximum height at which players may place blocks.
*/
private int maxBuildHeight;
private Set<Key> activeChunksSet = new HashSet<>();
/**
* Creates a new world from the options in the given WorldCreator.
*
* @param server The server for the world.
* @param creator The WorldCreator to use.
*/
public GlowWorld(GlowServer server, WorldCreator creator,
WorldStorageProvider worldStorageProvider) {
this.server = server;
// set up values from WorldCreator
name = creator.name();
environment = creator.environment();
worldType = creator.type();
generateStructures = creator.generateStructures();
generator = creator.generator();
storageProvider = worldStorageProvider;
storageProvider.setWorld(this);
populators = generator.getDefaultPopulators(this);
// set up values from server defaults
ticksPerAnimal = server.getTicksPerAnimalSpawns();
ticksPerMonster = server.getTicksPerMonsterSpawns();
monsterLimit = server.getMonsterSpawnLimit();
animalLimit = server.getAnimalSpawnLimit();
waterAnimalLimit = server.getWaterAnimalSpawnLimit();
ambientLimit = server.getAmbientSpawnLimit();
keepSpawnLoaded = server.keepSpawnLoaded();
populateAnchoredChunks = server.populateAnchoredChunks();
difficulty = server.getDifficulty();
maxBuildHeight = server.getMaxBuildHeight();
seaLevel = GlowServer.getWorldConfig().getInt(WorldConfig.Key.SEA_LEVEL);
worldBorder = new GlowWorldBorder(this);
// read in world data
WorldFinalValues values;
values = storageProvider.getMetadataService().readWorldData();
if (values != null) {
if (values.getSeed() == 0L) {
seed = creator.seed();
} else {
seed = values.getSeed();
}
uid = values.getUuid();
} else {
seed = creator.seed();
uid = UUID.randomUUID();
}
chunks = new ChunkManager(this, storageProvider.getChunkIoService(), generator);
structures = storageProvider.getStructureDataService().readStructuresData();
functions = storageProvider.getFunctionIoService().readFunctions().stream()
.collect(Collectors.toMap(CommandFunction::getFullName, function -> function));
server.addWorld(this);
server.getLogger().info("Preparing spawn for " + name + "...");
EventFactory.callEvent(new WorldInitEvent(this));
spawnChunkLock = keepSpawnLoaded ? newChunkLock("spawn") : null;
setKeepSpawnInMemory(keepSpawnLoaded);
server.getLogger().info("Preparing spawn for " + name + ": done");
EventFactory.callEvent(new WorldLoadEvent(this));
// pulse AI tasks
//aiTaskService = Executors.newScheduledThreadPool(1);
}
////////////////////////////////////////////////////////////////////////////
// Various internal mechanisms
/**
* Get the world chunk manager.
*
* @return The ChunkManager for the world.
*/
public ChunkManager getChunkManager() {
return chunks;
}
/**
* Get the world's parent server.
*
* @return The GlowServer for the world.
*/
public GlowServer getServer() {
return server;
}
/**
* Get a new chunk lock object a player or other party can use to keep chunks loaded.
*
* @param desc A description for this chunk lock.
* @return The ChunkLock.
*/
public ChunkLock newChunkLock(String desc) {
return new ChunkLock(chunks, name + ": " + desc);
}
/**
* Updates all the entities within this world.
*/
public void pulse() {
List<GlowEntity> allEntities = new ArrayList<>(entities.getAll());
List<GlowPlayer> players = new LinkedList<>();
activeChunksSet.clear();
// We should pulse our tickmap, so blocks get updated.
pulseTickMap();
// pulse players last so they actually see that other entities have
// moved. unfortunately pretty hacky. not a problem for players b/c
// their position is modified by session ticking.
for (GlowEntity entity : allEntities) {
if (entity instanceof GlowPlayer) {
players.add((GlowPlayer) entity);
updateActiveChunkCollection(entity);
} else {
entity.pulse();
}
}
updateBlocksInActiveChunks();
// why update blocks before Players or Entities? if there is a specific reason we should
// document it here.
pulsePlayers(players);
resetEntities(allEntities);
worldBorder.pulse();
updateWorldTime();
informPlayersOfTime();
updateOverworldWeather();
handleSleepAndWake(players);
saveWorld();
}
private void updateActiveChunkCollection(GlowEntity entity) {
// build a set of chunks around each player in this world, the
// server view distance is taken here
int radius = server.getViewDistance();
Location playerLocation = entity.getLocation();
if (playerLocation.getWorld() == this) {
int cx = playerLocation.getBlockX() >> 4;
int cz = playerLocation.getBlockZ() >> 4;
for (int x = cx - radius; x <= cx + radius; x++) {
for (int z = cz - radius; z <= cz + radius; z++) {
if (isChunkLoaded(cx, cz)) {
activeChunksSet.add(GlowChunk.Key.of(x, z));
}
}
}
}
}
private void updateBlocksInActiveChunks() {
for (Key key : activeChunksSet) {
int cx = key.getX();
int cz = key.getZ();
// check the chunk is loaded
if (isChunkLoaded(cx, cz)) {
GlowChunk chunk = getChunkAt(cx, cz);
// thunder
maybeStrikeLightningInChunk(cx, cz);
// block ticking
// we will choose 3 blocks per chunk's section
ChunkSection[] sections = chunk.getSections();
for (int i = 0; i < sections.length; i++) {
updateBlocksInSection(chunk, sections[i], i);
}
}
}
}
private void updateBlocksInSection(GlowChunk chunk, ChunkSection section, int i) {
if (section != null) {
for (int j = 0; j < 3; j++) {
int n = ThreadLocalRandom.current().nextInt();
int x = n & 0xF;
int z = n >> 8 & 0xF;
int y = n >> 16 & 0xF;
int type = section.getType(x, y, z) >> 4;
if (type != 0) { // filter air blocks
BlockType blockType = ItemTable.instance().getBlock(type);
// does this block needs random tick ?
if (blockType != null && blockType.canTickRandomly()) {
blockType.updateBlock(chunk.getBlock(x, y + (i << 4), z));
}
}
}
}
}
private void saveWorld() {
if (--saveTimer <= 0) {
saveTimer = AUTOSAVE_TIME;
chunks.unloadOldChunks();
if (autosave) {
save(true);
}
}
}
private void updateOverworldWeather() {
// only tick weather in a NORMAL world
if (environment == Environment.NORMAL) {
if (--rainingTicks <= 0) {
setStorm(!currentlyRaining);
}
if (--thunderingTicks <= 0) {
setThundering(!currentlyThundering);
}
updateWeather();
}
}
private void informPlayersOfTime() {
if (worldAge % (30 * TICKS_PER_SECOND) == 0) {
// Only send the time every 30 seconds; clients are smart.
getRawPlayers().forEach(GlowPlayer::sendTime);
}
}
// Tick the world age and time of day
private void updateWorldTime() {
worldAge++;
// worldAge is used to determine when to (periodically) update clients of server time
// (time of day - "time")
// also used to occasionally pulse some blocks (see "tickMap" and "requestPulse()")
// Modulus by 24000, the tick length of a day
if (gameRules.getBoolean("doDaylightCycle")) {
time = (time + 1) % DAY_LENGTH;
}
}
private void resetEntities(List<GlowEntity> entities) {
entities.forEach(GlowEntity::reset);
}
private void pulsePlayers(List<GlowPlayer> players) {
players.stream().filter(Objects::nonNull).forEach(GlowEntity::pulse);
}
private void handleSleepAndWake(List<GlowPlayer> players) {
// Skip checking for sleeping players if no one is online
if (!players.isEmpty()) {
// If the night is over, wake up all players
// Tick values for day/night time taken from the minecraft wiki
if (getTime() < 12541 || getTime() > 23458) {
wakeUpAllPlayers(players);
// no need to send them the time - handle that normally
} else { // otherwise check whether everyone is asleep
boolean skipNight = gameRules.getBoolean("doDaylightCycle")
&& areAllPlayersSleeping(players);
// check gamerule before iterating players (micro-optimization)
if (skipNight) {
skipRestOfNight(players);
}
}
}
}
private void skipRestOfNight(List<GlowPlayer> players) {
worldAge = (worldAge / DAY_LENGTH + 1) * DAY_LENGTH;
time = 0;
wakeUpAllPlayers(players, true);
// true = send time to all players because we just changed it (to 0), above
setStorm(false);
setThundering(false);
}
private void wakeUpAllPlayers(List<GlowPlayer> players) {
wakeUpAllPlayers(players, false);
}
private void wakeUpAllPlayers(List<GlowPlayer> players, boolean sendTime) {
for (GlowPlayer player : players) {
if (sendTime) {
player.sendTime();
}
if (player.isSleeping()) {
player.leaveBed(true);
}
}
}
private boolean areAllPlayersSleeping(List<GlowPlayer> players) {
for (GlowPlayer player : players) {
if (!(player.isSleeping() && player.getSleepTicks() >= 100) && !player
.isSleepingIgnored()) {
return false;
}
}
return true;
}
public void broadcastBlockChangeInRange(GlowChunk.Key chunkKey, BlockChangeMessage message) {
getRawPlayers().stream().filter(player -> player.canSeeChunk(chunkKey))
.forEach(player -> player.sendBlockChangeForce(message));
}
private void maybeStrikeLightningInChunk(int cx, int cz) {
if (environment == Environment.NORMAL && currentlyRaining && currentlyThundering) {
if (ThreadLocalRandom.current().nextInt(100000) == 0) {
strikeLightningInChunk(cx, cz);
}
}
}
private void strikeLightningInChunk(int cx, int cz) {
int n = ThreadLocalRandom.current().nextInt();
// get lightning target block
int x = (cx << 4) + (n & 0xF);
int z = (cz << 4) + (n >> 8 & 0xF);
int y = getHighestBlockYAt(x, z);
// search for living entities in a 6×6×h (there's an error in the wiki!) region from 3
// below the
// target block up to the world height
BoundingBox searchBox = BoundingBox
.fromPositionAndSize(new Vector(x, y, z), new Vector(0, 0, 0));
Vector vec = new Vector(3, 3, 3);
Vector vec2 = new Vector(0, getMaxHeight(), 0);
searchBox.minCorner.subtract(vec);
searchBox.maxCorner.add(vec).add(vec2);
List<LivingEntity> livingEntities = new LinkedList<>();
// make sure entity can see sky
getEntityManager().getEntitiesInside(searchBox, null).stream()
.filter(entity -> entity instanceof LivingEntity && !entity.isDead())
.forEach(entity -> {
Vector pos = entity.getLocation().toVector();
int minY = getHighestBlockYAt(pos.getBlockX(), pos.getBlockZ());
if (pos.getBlockY() >= minY) {
livingEntities.add((LivingEntity) entity);
}
});
// re-target lightning if required
if (!livingEntities.isEmpty()) {
// randomly choose an entity
LivingEntity entity = livingEntities
.get(ThreadLocalRandom.current().nextInt(livingEntities.size()));
// re-target lightning on this living entity
Vector newTarget = entity.getLocation().toVector();
x = newTarget.getBlockX();
z = newTarget.getBlockZ();
y = newTarget.getBlockY();
}
// lightning strike if the target block is under rain
if (GlowBiomeClimate.isRainy(getBiome(x, z), x, y, z)) {
strikeLightning(new Location(this, x, y, z));
}
}
/**
* Calculates how much the rays from the location to the entity's bounding box is blocked.
*
* @param location The location for the rays to start
* @param entity The entity that's bounding box is the ray's end point
* @return a value between 0 and 1, where 0 = all rays blocked and 1 = all rays unblocked
*/
public float rayTrace(Location location, GlowEntity entity) {
// TODO: calculate how much of the entity is visible (not blocked by blocks) from the
// location
/*
* To calculate this step through the entity's bounding box and check whether the ray to
* the point
* in the bounding box is blocked.
*
* Return (unblockedRays / allRays)
*/
return RayUtil.getExposure(location, entity.getLocation());
}
/**
* Gets the entity manager.
*
* @return The entity manager.
*/
public EntityManager getEntityManager() {
return entities;
}
public Collection<GlowPlayer> getRawPlayers() {
return entities.getAll(GlowPlayer.class);
}
////////////////////////////////////////////////////////////////////////////
// Entity lists
@Override
public List<Player> getPlayers() {
return new ArrayList<>(entities.getAll(GlowPlayer.class));
}
/**
* Returns a list of entities within a bounding box centered around a Location.
*
* <p>Some implementations may impose artificial restrictions on the size of the search bounding
* box.
*
* @param location The center of the bounding box
* @param x 1/2 the size of the box along x axis
* @param y 1/2 the size of the box along y axis
* @param z 1/2 the size of the box along z axis
* @return the collection of entities near location. This will always be a non-null collection.
*/
@Override
public Collection<Entity> getNearbyEntities(Location location, double x, double y, double z) {
Vector minCorner = new Vector(
location.getX() - x, location.getY() - y, location.getZ() - z);
Vector maxCorner = new Vector(
location.getX() + x, location.getY() + y, location.getZ() + z);
BoundingBox searchBox = BoundingBox.fromCorners(minCorner, maxCorner); // TODO: test
GlowEntity except = null;
return entities.getEntitiesInside(searchBox, except);
}
@Override
public List<Entity> getEntities() {
return new ArrayList<>(entities.getAll());
}
@Override
public List<LivingEntity> getLivingEntities() {
return entities.getAll().stream().filter(e -> e instanceof GlowLivingEntity)
.map(e -> (GlowLivingEntity) e).collect(Collectors.toCollection(LinkedList::new));
}
@Override
@Deprecated
@SuppressWarnings("unchecked")
public <T extends Entity> Collection<T> getEntitiesByClass(Class<T>... classes) {
return (Collection<T>) getEntitiesByClasses(classes);
}
@Override
@SuppressWarnings("unchecked")
public <T extends Entity> Collection<T> getEntitiesByClass(Class<T> cls) {
return entities.getAll().stream().filter(e -> cls.isAssignableFrom(e.getClass()))
.map(e -> (T) e).collect(Collectors.toCollection(ArrayList::new));
}
@Override
public Collection<Entity> getEntitiesByClasses(Class<?>... classes) {
ArrayList<Entity> result = new ArrayList<>();
for (Entity e : entities.getAll()) {
for (Class<?> cls : classes) {
if (cls.isAssignableFrom(e.getClass())) {
result.add(e);
break;
}
}
}
return result;
}
////////////////////////////////////////////////////////////////////////////
// Various malleable world properties
@Override
public Location getSpawnLocation() {
return spawnLocation.clone();
}
@Override
public boolean setSpawnLocation(Location newSpawn) {
return setSpawnLocation(newSpawn, true);
}
@Override
public boolean setSpawnLocation(int x, int y, int z) {
return setSpawnLocation(new Location(this, x, y, z), true);
}
/**
* Sets the spawn location of the world.
*
* @param newSpawn the new spawn location
* @param anchor if true, the spawn is never unloaded while the world is running
* @return true if the spawn location has changed
*/
public boolean setSpawnLocation(Location newSpawn, boolean anchor) {
Location oldSpawn = spawnLocation;
if (newSpawn.equals(oldSpawn)) {
return false;
}
spawnLocation = newSpawn;
if (anchor) {
setKeepSpawnInMemory(keepSpawnLoaded);
}
EventFactory.callEvent(new SpawnChangeEvent(this, oldSpawn));
return true;
}
public boolean setSpawnLocation(int x, int y, int z, boolean anchor) {
return setSpawnLocation(new Location(this, x, y, z), anchor);
}
@Override
public boolean getPVP() {
return pvpAllowed;
}
@Override
public void setPVP(boolean pvp) {
pvpAllowed = pvp;
}
@Override
public boolean getKeepSpawnInMemory() {
return keepSpawnLoaded;
}
@Override
public void setKeepSpawnInMemory(boolean keepLoaded) {
keepSpawnLoaded = keepLoaded;
boolean needSpawn = spawnLocation == null;
if (needSpawn) {
// find a spawn if needed
Location spawn = generator.getFixedSpawnLocation(this, ThreadLocalRandom.current());
// we're already going to anchor if told to, so don't request another anchor
if (spawn == null) {
// determine a location randomly
int spawnX = ThreadLocalRandom.current().nextInt(256) - 128;
int spawnZ = ThreadLocalRandom.current().nextInt(256) - 128;
getChunkAt(spawnX >> 4, spawnZ >> 4)
.load(true); // I'm not sure there's a sane way around this
for (int tries = 0; tries < 1000 && !generator.canSpawn(this, spawnX, spawnZ);
++tries) {
spawnX += ThreadLocalRandom.current().nextInt(256) - 128;
spawnZ += ThreadLocalRandom.current().nextInt(256) - 128;
}
setSpawnLocation(spawnX, getHighestBlockYAt(spawnX, spawnZ), spawnZ);
needSpawn = false;
} else {
setSpawnLocation(spawn.getBlockX(), spawn.getBlockY(), spawn.getBlockZ(), false);
}
}
if (spawnChunkLock == null) {
if (keepSpawnLoaded) {
spawnChunkLock = newChunkLock("spawn");
prepareSpawn();
}
} else {
// update the chunk lock as needed
spawnChunkLock.clear();
if (keepSpawnLoaded) {
prepareSpawn();
} else {
// attempt to immediately unload the spawn
chunks.unloadOldChunks();
spawnChunkLock = null;
}
}
if (needSpawn) {
setSpawnLocation(spawnLocation.getBlockX(), getHighestBlockYAt(spawnLocation
.getBlockX(), spawnLocation.getBlockZ()), spawnLocation.getBlockZ(), false);
}
}
private void prepareSpawn() {
int centerX = spawnLocation.getBlockX() >> 4;
int centerZ = spawnLocation.getBlockZ() >> 4;
int radius = 4 * server.getViewDistance() / 3;
long loadTime = System.currentTimeMillis();
int total = ((radius << 1) + 1) * ((radius << 1) + 1);
int current = 0;
for (int x = centerX - radius; x <= centerX + radius; ++x) {
for (int z = centerZ - radius; z <= centerZ + radius; ++z) {
++current;
if (populateAnchoredChunks) {
getChunkManager().forcePopulation(x, z);
} else {
loadChunk(x, z);
}
spawnChunkLock.acquire(GlowChunk.Key.of(x, z));
if (System.currentTimeMillis() >= loadTime + 1000) {
int progress = 100 * current / total;
GlowServer.logger.info("Preparing spawn for " + name + ": " + progress + "%");
loadTime = System.currentTimeMillis();
}
}
}
}
@Override
public boolean isAutoSave() {
return autosave;
}
@Override
public void setAutoSave(boolean value) {
autosave = value;
}
@Override
public Difficulty getDifficulty() {
return difficulty;
}
@Override
public void setDifficulty(Difficulty difficulty) {
this.difficulty = difficulty;
ServerDifficultyMessage message = new ServerDifficultyMessage(difficulty);
for (GlowPlayer player : getRawPlayers()) {
player.getSession().send(message);
}
}
////////////////////////////////////////////////////////////////////////////
// Entity spawning properties
@Override
public void setSpawnFlags(boolean allowMonsters, boolean allowAnimals) {
spawnMonsters = allowMonsters;
spawnAnimals = allowAnimals;
}
@Override
public boolean getAllowAnimals() {
return spawnAnimals;
}
@Override
public boolean getAllowMonsters() {
return spawnMonsters;
}
@Override
public long getTicksPerAnimalSpawns() {
return ticksPerAnimal;
}
@Override
public void setTicksPerAnimalSpawns(int ticksPerAnimalSpawns) {
ticksPerAnimal = ticksPerAnimalSpawns;
}
@Override
public long getTicksPerMonsterSpawns() {
return ticksPerMonster;
}
@Override
public void setTicksPerMonsterSpawns(int ticksPerMonsterSpawns) {
ticksPerMonster = ticksPerMonsterSpawns;
}
@Override
public int getMonsterSpawnLimit() {
return monsterLimit;
}
@Override
public void setMonsterSpawnLimit(int limit) {
monsterLimit = limit;
}
@Override
public int getAnimalSpawnLimit() {
return animalLimit;
}
@Override
public void setAnimalSpawnLimit(int limit) {
animalLimit = limit;
}
@Override
public int getWaterAnimalSpawnLimit() {
return waterAnimalLimit;
}
@Override
public void setWaterAnimalSpawnLimit(int limit) {
waterAnimalLimit = limit;
}
@Override
public int getAmbientSpawnLimit() {
return ambientLimit;
}
@Override
public void setAmbientSpawnLimit(int limit) {
ambientLimit = limit;
}
////////////////////////////////////////////////////////////////////////////
// Various fixed world properties
@Override
public Environment getEnvironment() {
return environment;
}
@Override
public long getSeed() {
return seed;
}
@Override
public UUID getUID() {
return uid;
}
@Override
public String getName() {
return name;
}
@Override
public int getMaxHeight() {
return maxBuildHeight;
}
@Override
public int getSeaLevel() {
if (worldType == WorldType.FLAT) {
return 4;
} else if (environment == Environment.THE_END) {
return 50;
} else {
return seaLevel;
}
}
@Override
public boolean canGenerateStructures() {
return generateStructures;
}
////////////////////////////////////////////////////////////////////////////
// force-save
@Override
public void save() {
save(false);
}
/**
* Saves world to disk synchronously or asynchronously.
*
* @param async if true, save asynchronously
*/
public void save(boolean async) {
EventFactory.callEvent(new WorldSaveEvent(this));
// save metadata
writeWorldData(async);
// save chunks
maybeAsync(async, () -> {
for (GlowChunk chunk : chunks.getLoadedChunks()) {
chunks.performSave(chunk);
}
});
// save players
for (GlowPlayer player : getRawPlayers()) {
player.saveData(async);
}
}
////////////////////////////////////////////////////////////////////////////
// map generation
@Override
public ChunkGenerator getGenerator() {
return chunks.getGenerator();
}
@Override
public List<BlockPopulator> getPopulators() {
return populators;
}
@Override
public boolean generateTree(Location location, TreeType type) {
return generateTree(location, type, null);
}
@Override
public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) {
BlockStateDelegate blockStateDelegate = new BlockStateDelegate();
if (GlowTree.newInstance(type, ThreadLocalRandom.current(), blockStateDelegate)
.generate(loc)) {
List<BlockState> blockStates = new ArrayList<>(blockStateDelegate.getBlockStates());
StructureGrowEvent growEvent
= new StructureGrowEvent(loc, type, false, null, blockStates);
EventFactory.callEvent(growEvent);
if (!growEvent.isCancelled()) {
for (BlockState state : blockStates) {
state.update(true);
if (delegate != null) {
delegate.setTypeIdAndData(state.getX(), state.getY(), state.getZ(), state
.getTypeId(), state.getRawData());
}
}
return true;
}
}
return false;
}
public Map<Integer, GlowStructure> getStructures() {
return structures;
}
////////////////////////////////////////////////////////////////////////////
// get block, chunk, id, highest methods with coords
@Override
public int getEntityCount() {
return getEntities().size();
}
@Override
@Deprecated
public int getTileEntityCount() {
return getBlockEntityCount();
}
/**
* Returns the number of block entities in loaded chunks.
*
* @return the number of block entities
*/
public int getBlockEntityCount() {
int length = 0;
for (GlowChunk chunk : getChunkManager().getLoadedChunks()) {
length += chunk.getBlockEntities().length;
}
return length;
}
@Override
@Deprecated
public int getTickableTileEntityCount() {
return getTickableBlockEntityCount();
}
/**
* Returns the number of tickable block entities in loaded chunks.
*
* @return the number of tickable block entities
*/
public int getTickableBlockEntityCount() {
// TODO: distinguish between block entity types
int length = 0;
for (GlowChunk chunk : getChunkManager().getLoadedChunks()) {
length += chunk.getBlockEntities().length;
}
return length;
}
@Override
public int getChunkCount() {
return getChunkManager().getLoadedChunks().length;
}
@Override
public int getPlayerCount() {
return getPlayers().size();
}
////////////////////////////////////////////////////////////////////////////
// get block, chunk, id, highest with locations
@Override
public GlowBlock getBlockAt(Location location) {
return getBlockAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
@Override
public GlowBlock getBlockAt(int x, int y, int z) {
return new GlowBlock(getChunkAt(x >> 4, z >> 4), x, y, z);
}
@Override
public int getBlockTypeIdAt(Location location) {
return getBlockTypeIdAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
@Override
public int getBlockTypeIdAt(int x, int y, int z) {
return getChunkAt(x >> 4, z >> 4).getType(x & 0xF, z & 0xF, y);
}
@Override
public int getHighestBlockYAt(Location location) {
return getHighestBlockYAt(location.getBlockX(), location.getBlockZ());
}
@Override
public int getHighestBlockYAt(int x, int z) {
return getChunkAt(x >> 4, z >> 4).getHeight(x & 0xf, z & 0xf);
}
@Override
public Block getHighestBlockAt(int x, int z) {
return getBlockAt(x, getHighestBlockYAt(x, z), z);
}
@Override
public Block getHighestBlockAt(Location location) {
return getBlockAt(location.getBlockX(), getHighestBlockYAt(location), location.getBlockZ());
}
/**
* Gets the lowest block at the given {@link Location} such that the block
* and all blocks above it are either air or one of the given materials.
*
* @param location Coordinates to get the highest block
* @param except Blocks to exclude in addition to air
* @return Highest non-empty block
*/
public Block getHighestBlockAt(Location location, Material... except) {
Block block = getHighestBlockAt(location);
List<Material> array = Arrays.asList(except);
for (int i = 0; i < 6; i++) {
block = block.getLocation().clone().subtract(0, i == 0 ? 0 : 1, 0).getBlock();
if (block.getType() == Material.AIR || array.contains(block.getType())) {
continue;
}
return block;
}
return getHighestBlockAt(location);
}
@Override
public Chunk getChunkAt(Location location) {
return getChunkAt(location.getBlockX() >> 4, location.getBlockZ() >> 4);
}
@Override
public GlowChunk getChunkAt(int x, int z) {
return chunks.getChunk(x, z);
}
@Override
public Chunk getChunkAt(Block block) {
return getChunkAt(block.getX() >> 4, block.getZ() >> 4);
}
@Override
public void getChunkAtAsync(int x, int z, ChunkLoadCallback cb) {
Bukkit.getServer().getScheduler()
.runTaskAsynchronously(null, () -> cb.onLoad(chunks.getChunk(x, z)));
}
@Override
public void getChunkAtAsync(Location location, ChunkLoadCallback cb) {
getChunkAtAsync(location.getBlockX() >> 4, location.getBlockZ() >> 4, cb);
}
@Override
public void getChunkAtAsync(Block block, ChunkLoadCallback cb) {
getChunkAtAsync(block.getX() >> 4, block.getZ() >> 4, cb);
}
////////////////////////////////////////////////////////////////////////////
// Chunk loading and unloading
@Override
public boolean isChunkLoaded(Chunk chunk) {
return chunk.isLoaded();
}
@Override
public boolean isChunkLoaded(int x, int z) {
return chunks.isChunkLoaded(x, z);
}
@Override
public boolean isChunkInUse(int x, int z) {
return chunks.isChunkInUse(x, z);
}
@Override
public Chunk[] getLoadedChunks() {
return chunks.getLoadedChunks();
}
@Override
public void loadChunk(Chunk chunk) {
chunk.load();
}
@Override
public void loadChunk(int x, int z) {
getChunkAtAsync(x, z, Chunk::load);
}
@Override
public boolean loadChunk(int x, int z, boolean generate) {
return getChunkAt(x, z).load(generate);
}
@Override
public boolean unloadChunk(Chunk chunk) {
return chunk.unload();
}
@Override
public boolean unloadChunk(int x, int z) {
return unloadChunk(x, z, true);
}
@Override
public boolean unloadChunk(int x, int z, boolean save) {
return unloadChunk(x, z, save, true);
}
@Override
public boolean unloadChunk(int x, int z, boolean save, boolean safe) {
return !isChunkLoaded(x, z) || getChunkAt(x, z).unload(save, safe);
}
@Override
public boolean unloadChunkRequest(int x, int z) {
return unloadChunkRequest(x, z, true);
}
@Override
public boolean unloadChunkRequest(int x, int z, boolean safe) {
if (safe && isChunkInUse(x, z)) {
return false;
}
server.getScheduler().runTask(null, () -> unloadChunk(x, z, safe));
return true;
}
@Override
public boolean regenerateChunk(int x, int z) {
if (!chunks.forceRegeneration(x, z)) {
return false;
}
refreshChunk(x, z);
return true;
}
@Override
public boolean refreshChunk(int x, int z) {
if (!isChunkLoaded(x, z)) {
return false;
}
Key key = GlowChunk.Key.of(x, z);
boolean result = false;
for (GlowPlayer player : getRawPlayers()) {
if (player.canSeeChunk(key)) {
player.getSession().send(getChunkAt(x, z).toMessage());
result = true;
}
}
return result;
}
@Override
public ChunkSnapshot getEmptyChunkSnapshot(int x, int z, boolean includeBiome,
boolean includeBiomeTempRain) {
return new EmptySnapshot(x, z, this, includeBiome, includeBiomeTempRain);
}
////////////////////////////////////////////////////////////////////////////
// Biomes
@Override
public Biome getBiome(int x, int z) {
if (environment == Environment.THE_END) {
return Biome.SKY;
} else if (environment == Environment.NETHER) {
return Biome.HELL;
}
return GlowBiome.getBiome(getChunkAt(x >> 4, z >> 4).getBiome(x & 0xF, z & 0xF));
}
@Override
public void setBiome(int x, int z, Biome bio) {
getChunkAtAsync(
x >> 4, z >> 4, chunk -> ((GlowChunk) chunk)
.setBiome(x & 0xF, z & 0xF, GlowBiome.getId(bio)));
}
@Override
public double getTemperature(int x, int z) {
return GlowBiomeClimate.getBiomeTemperature(getBiome(x, z));
}
@Override
public double getHumidity(int x, int z) {
return GlowBiomeClimate.getBiomeHumidity(getBiome(x, z));
}
////////////////////////////////////////////////////////////////////////////
// Entity spawning
@Override
public <T extends Entity> T spawn(Location location,
Class<T> clazz) throws IllegalArgumentException {
return (T) spawn(location, EntityRegistry.getEntity(clazz), SpawnReason.CUSTOM);
}
@Override
public <T extends Entity> T spawn(Location location, Class<T> clazz,
Consumer<T> function) throws IllegalArgumentException {
return null; // TODO: work on type mismatches
}
/**
* Spawns an entity.
*
* @param location the {@link Location} to spawn the entity at
* @param clazz the class of the {@link Entity} to spawn
* @param reason the reason for the spawning of the entity
* @return an instance of the spawned {@link Entity}
* @throws IllegalArgumentException TODO: document the reason this can happen
*/
public GlowEntity spawn(Location location, Class<? extends GlowEntity> clazz,
SpawnReason reason) throws IllegalArgumentException {
GlowEntity entity = null;
if (TNTPrimed.class.isAssignableFrom(clazz)) {
entity = new GlowTntPrimed(location, null);
}
if (entity == null) {
try {
Constructor<? extends GlowEntity> constructor = clazz
.getConstructor(Location.class);
entity = constructor.newInstance(location);
GlowEntity impl = entity;
// function.accept(entity); TODO: work on type mismatches
EntitySpawnEvent spawnEvent = null;
if (entity instanceof LivingEntity) {
spawnEvent = EventFactory
.callEvent(new CreatureSpawnEvent((LivingEntity) entity, reason));
} else if (!(entity instanceof Item)) { // ItemSpawnEvent is called elsewhere
spawnEvent = EventFactory.callEvent(new EntitySpawnEvent(entity));
}
if (spawnEvent != null && spawnEvent.isCancelled()) {
// TODO: separate spawning and construction for better event cancellation
entity.remove();
} else {
List<Message> spawnMessage = entity.createSpawnMessage();
getRawPlayers().stream().filter(player -> player.canSeeEntity(impl))
.forEach(player -> player.getSession().sendAll(spawnMessage
.toArray(new Message[spawnMessage.size()])));
}
} catch (NoSuchMethodException e) {
GlowServer.logger.log(Level.WARNING, "Invalid entity spawn: ", e);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
e) {
GlowServer.logger.log(Level.SEVERE, "Unable to spawn entity: ", e);
}
}
if (entity != null) {
return entity;
}
throw new UnsupportedOperationException("Not supported yet.");
}
/**
* Spawn a custom entity at the given {@link Location}.
*
* @param location the {@link Location} to spawn the entity at
* @param id the id of the custom entity
* @param <T> the class of the {@link Entity} to spawn
* @return an instance of the spawned {@link Entity}
*/
public <T extends Entity> T spawnCustomEntity(Location location,
String id) throws IllegalArgumentException {
return spawnCustomEntity(location, id, SpawnReason.CUSTOM);
}
/**
* Spawn a custom entity at the given {@link Location}, with the given {@link SpawnReason}.
*
* @param location the {@link Location} to spawn the entity at
* @param id the id of the custom entity
* @param reason the reason for the spawning of the entity
* @param <T> the class of the {@link Entity} to spawn
* @return an instance of the spawned {@link Entity}
*/
@SuppressWarnings("unchecked")
public <T extends Entity> T spawnCustomEntity(Location location, String id,
SpawnReason reason) throws IllegalArgumentException {
Preconditions.checkNotNull(id);
CustomEntityDescriptor descriptor = EntityRegistry.getCustomEntityDescriptor(id);
if (descriptor == null) {
throw new IllegalArgumentException(
"Could not find a custom entity descriptor for the given id '" + id + "'");
}
return (T) spawn(location, descriptor.getEntityClass(), reason);
}
@Override
public GlowItem dropItem(Location location, ItemStack item) {
GlowItem entity = new GlowItem(location, item);
ItemSpawnEvent event = EventFactory.callEvent(new ItemSpawnEvent(entity));
if (event.isCancelled()) {
entity.remove();
}
return entity;
}
@Override
public GlowItem dropItemNaturally(Location location, ItemStack item) {
double xs = ThreadLocalRandom.current().nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
double ys = ThreadLocalRandom.current().nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
double zs = ThreadLocalRandom.current().nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
location = location.clone().add(xs, ys, zs);
GlowItem dropItem = dropItem(location, item);
dropItem.setVelocity(new Vector(0, 0.01F, 0));
return dropItem;
}
@Override
public Arrow spawnArrow(Location location, Vector velocity, float speed, float spread) {
// Transformative magic
Vector randVec = new Vector(ThreadLocalRandom.current().nextGaussian(), ThreadLocalRandom
.current().nextGaussian(), ThreadLocalRandom.current().nextGaussian());
randVec.multiply(0.0075 * spread);
velocity.normalize();
velocity.add(randVec);
velocity.multiply(speed);
// yaw = Math.atan2(x, z) * 180.0D / 3.1415927410125732D;
// pitch = Math.atan2(y, Math.sqrt(x * x + z * z)) * 180.0D / 3.1415927410125732D
Arrow arrow = spawn(location, Arrow.class);
arrow.setVelocity(velocity);
return arrow;
}
@Override
public <T extends Arrow> T spawnArrow(Location location, Vector direction, float speed,
float spread, Class<T> clazz) {
return null;
}
@Override
public FallingBlock spawnFallingBlock(Location location,
MaterialData data) throws IllegalArgumentException {
return spawnFallingBlock(location, data.getItemType(), data.getData());
}
@Override
public FallingBlock spawnFallingBlock(Location location, Material material,
byte data) throws IllegalArgumentException {
if (location == null || material == null) {
throw new IllegalArgumentException();
}
return new GlowFallingBlock(location, material, data);
}
@Override
public FallingBlock spawnFallingBlock(Location location, int blockId,
byte blockData) throws IllegalArgumentException {
Material material = Material.getMaterial(blockId);
return spawnFallingBlock(location, material, blockData);
}
@Override
public Entity spawnEntity(Location loc, EntityType type) {
return spawn(loc, type.getEntityClass());
}
private GlowLightningStrike strikeLightningFireEvent(Location loc, boolean effect,
boolean isSilent) {
GlowLightningStrike strike = new GlowLightningStrike(loc, effect, isSilent);
LightningStrikeEvent event = new LightningStrikeEvent(this, strike);
if (EventFactory.callEvent(event).isCancelled()) {
return null;
}
return strike;
}
@Override
public GlowLightningStrike strikeLightning(Location loc) {
return strikeLightningFireEvent(loc, false, false);
}
@Override
public GlowLightningStrike strikeLightningEffect(Location loc) {
return strikeLightningFireEvent(loc, true, false);
}
////////////////////////////////////////////////////////////////////////////
// Time
@Override
public long getTime() {
return time;
}
@Override
public void setTime(long time) {
this.time = (time % DAY_LENGTH + DAY_LENGTH) % DAY_LENGTH;
getRawPlayers().forEach(GlowPlayer::sendTime);
}
@Override
public long getFullTime() {
return worldAge;
}
@Override
public void setFullTime(long time) {
worldAge = time;
}
////////////////////////////////////////////////////////////////////////////
// Weather
@Override
public boolean hasStorm() {
return currentlyRaining;
}
@Override
public void setStorm(boolean hasStorm) {
// call event
WeatherChangeEvent event = new WeatherChangeEvent(this, hasStorm);
if (EventFactory.callEvent(event).isCancelled()) {
return;
}
// change weather
boolean previouslyRaining = currentlyRaining;
currentlyRaining = hasStorm;
// Numbers borrowed from CraftBukkit.
if (currentlyRaining) {
setWeatherDuration(
ThreadLocalRandom.current().nextInt(HALF_DAY_IN_TICKS) + HALF_DAY_IN_TICKS);
} else {
setWeatherDuration(
ThreadLocalRandom.current().nextInt(WEEK_IN_TICKS) + HALF_DAY_IN_TICKS);
}
// update players
if (previouslyRaining != currentlyRaining) {
getRawPlayers().forEach(GlowPlayer::sendWeather);
}
}
@Override
public int getWeatherDuration() {
return rainingTicks;
}
@Override
public void setWeatherDuration(int duration) {
rainingTicks = duration;
}
@Override
public boolean isThundering() {
return currentlyThundering;
}
@Override
public void setThundering(boolean thundering) {
// call event
ThunderChangeEvent event = new ThunderChangeEvent(this, thundering);
if (EventFactory.callEvent(event).isCancelled()) {
return;
}
// change weather
currentlyThundering = thundering;
// Numbers borrowed from CraftBukkit.
if (currentlyThundering) {
setThunderDuration(ThreadLocalRandom.current().nextInt(HALF_DAY_IN_TICKS)
+ 180 * TICKS_PER_SECOND);
} else {
setThunderDuration(
ThreadLocalRandom.current().nextInt(WEEK_IN_TICKS) + HALF_DAY_IN_TICKS);
}
}
@Override
public int getThunderDuration() {
return thunderingTicks;
}
@Override
public void setThunderDuration(int duration) {
thunderingTicks = duration;
}
public float getRainDensity() {
return currentRainDensity;
}
public float getSkyDarkness() {
return currentSkyDarkness;
}
private void updateWeather() {
float previousRainDensity = currentRainDensity;
float previousSkyDarkness = currentSkyDarkness;
float rainDensityModifier = currentlyRaining ? .01F : -.01F;
float skyDarknessModifier = currentlyThundering ? .01F : -.01F;
currentRainDensity = Math.max(0, Math.min(1, previousRainDensity + rainDensityModifier));
currentSkyDarkness = Math.max(0, Math.min(1, previousSkyDarkness + skyDarknessModifier));
if (previousRainDensity != currentRainDensity) {
getRawPlayers().forEach(GlowPlayer::sendRainDensity);
}
if (previousSkyDarkness != currentSkyDarkness) {
getRawPlayers().forEach(GlowPlayer::sendSkyDarkness);
}
}
////////////////////////////////////////////////////////////////////////////
// Explosions
@Override
public boolean createExplosion(Location loc, float power) {
return createExplosion(loc, power, false);
}
@Override
public boolean createExplosion(Location loc, float power, boolean setFire) {
return createExplosion(loc.getX(), loc.getY(), loc.getZ(), power, setFire, true);
}
@Override
public boolean createExplosion(double x, double y, double z, float power) {
return createExplosion(x, y, z, power, false, true);
}
@Override
public boolean createExplosion(double x, double y, double z, float power, boolean setFire) {
return createExplosion(x, y, z, power, setFire, true);
}
@Override
public boolean createExplosion(double x, double y, double z, float power, boolean setFire,
boolean breakBlocks) {
return createExplosion(null, x, y, z, power, setFire, breakBlocks);
}
/**
* Create an explosion with a specific entity as the source.
*
* @param source The entity to treat as the source, or null.
* @param x X coordinate
* @param y Y coordinate
* @param z Z coordinate
* @param power The power of explosion, where 4F is TNT
* @param incendiary Whether or not to set blocks on fire
* @param breakBlocks Whether or not to have blocks be destroyed
* @return false if explosion was canceled, otherwise true
*/
public boolean createExplosion(Entity source, double x, double y, double z, float power,
boolean incendiary, boolean breakBlocks) {
Explosion explosion = new Explosion(source, this, x, y, z, power, incendiary, breakBlocks);
return explosion.explodeWithEvent();
}
////////////////////////////////////////////////////////////////////////////
// Effects
@Override
public void playEffect(Location location, Effect effect, int data) {
playEffect(location, effect, data, 64);
}
@Override
public void playEffect(Location location, Effect effect, int data, int radius) {
int radiusSquared = radius * radius;
getRawPlayers().stream()
.filter(player -> player.getLocation().distanceSquared(location) <= radiusSquared)
.forEach(player -> player.playEffect(location, effect, data));
}
@Override
public <T> void playEffect(Location location, Effect effect, T data) {
playEffect(location, effect, data, 64);
}
@Override
public <T> void playEffect(Location location, Effect effect, T data, int radius) {
playEffect(location, effect, GlowEffect.getDataValue(effect, data), radius);
}
/**
* Plays an effect to all but one player within a given radius around a location.
*
* @param location the {@link Location} around which players must be to
* hear the effect
* @param effect the {@link Effect}
* @param data a data bit needed for some effects
* @param radius the radius around the location
* @param exclude the player who won't see the effect
*/
public void playEffectExceptTo(Location location, Effect effect, int data, int radius,
Player exclude) {
int radiusSquared = radius * radius;
getRawPlayers().stream().filter(player -> !player.equals(exclude)
&& player.getLocation().distanceSquared(location) <= radiusSquared)
.forEach(player -> player.playEffect(location, effect, data));
}
@Override
public void playSound(Location location, Sound sound, float volume, float pitch) {
playSound(location, sound, GlowSound
.getSoundCategory(GlowSound.getVanillaId(sound)), volume, pitch);
}
@Override
public void playSound(Location location, String sound, float volume, float pitch) {
playSound(location, GlowSound.getVanillaSound(sound), volume, pitch);
}
@Override
public void playSound(Location location, Sound sound, SoundCategory category, float volume,
float pitch) {
if (location == null || sound == null) {
return;
}
double radiusSquared = Math.pow(volume * 16, 2);
getRawPlayers().stream()
.filter(player -> player.getLocation().distanceSquared(location) <= radiusSquared)
.forEach(player -> player.playSound(location, sound, category, volume, pitch));
}
@Override
public void playSound(Location location, String sound, SoundCategory category, float volume,
float pitch) {
playSound(location, GlowSound.getVanillaSound(sound), category, volume, pitch);
}
@Override
public Spigot spigot() {
return spigot;
}
/**
* Displays the given particle to all players.
*
* @param loc the location
* @param particle the particle type
* @param offsetX TODO: document this parameter
* @param offsetY TODO: document this parameter
* @param offsetZ TODO: document this parameter
* @param speed TODO: document this parameter
* @param amount the number of particles
*/
//@Override
public void showParticle(Location loc, Effect particle, float offsetX, float offsetY,
float offsetZ, float speed, int amount) {
int radius;
if (GlowParticle.isLongDistance(particle)) {
radius = 48;
} else {
radius = 16;
}
showParticle(loc, particle, particle
.getId(), 0, offsetX, offsetY, offsetZ, speed, amount, radius);
}
/**
* Displays the given particle to all players.
*
* @param loc the location
* @param particle the particle type
* @param id the block or item type ID
* @param data the block or item data
* @param offsetX TODO: document this parameter
* @param offsetY TODO: document this parameter
* @param offsetZ TODO: document this parameter
* @param speed TODO: document this parameter
* @param amount the number of particles
*/
//@Override
public void showParticle(Location loc, Effect particle, int id, int data, float offsetX,
float offsetY, float offsetZ, float speed, int amount, int radius) {
if (loc == null || particle == null) {
return;
}
double radiusSquared = radius * radius;
getRawPlayers().stream()
.filter(player -> player.getLocation().distanceSquared(loc) <= radiusSquared)
.forEach(player -> player.spigot()
.playEffect(loc, particle, id, data, offsetX, offsetY, offsetZ, speed,
amount, radius));
}
/**
* Save the world data using the metadata service.
*
* @param async Whether to write asynchronously.
*/
private void writeWorldData(boolean async) {
maybeAsync(async, () -> {
try {
storageProvider.getMetadataService().writeWorldData();
storageProvider.getScoreboardIoService().save();
} catch (IOException e) {
server.getLogger().severe("Could not save metadata for world: " + getName());
e.printStackTrace();
}
storageProvider.getStructureDataService().writeStructuresData(structures);
});
}
////////////////////////////////////////////////////////////////////////////
// Level data write
/**
* Execute a runnable, optionally asynchronously.
*
* @param async Whether to run the runnable in an asynchronous task.
* @param runnable The runnable to run.
*/
private void maybeAsync(boolean async, Runnable runnable) {
if (async) {
server.getScheduler().runTaskAsynchronously(null, runnable);
} else {
runnable.run();
}
}
/**
* Unloads the world.
*
* @return true if successful
*/
public boolean unload() {
// terminate task service
//aiTaskService.shutdown();
if (EventFactory.callEvent(new WorldUnloadEvent(this)).isCancelled()) {
return false;
}
try {
storageProvider.getChunkIoService().unload();
storageProvider.getScoreboardIoService().unload();
} catch (IOException e) {
return false;
}
return true;
}
/**
* Get the storage provider for the world.
*
* @return The {@link WorldStorageProvider}.
*/
public WorldStorageProvider getStorage() {
return storageProvider;
}
/**
* Get the world folder.
*
* @return world folder
*/
@Override
public File getWorldFolder() {
return storageProvider.getFolder();
}
@Override
public String[] getGameRules() {
return gameRules.getKeys();
}
////////////////////////////////////////////////////////////////////////////
// Game rules
@Override
public String getGameRuleValue(String rule) {
return gameRules.getString(rule);
}
@Override
public boolean setGameRuleValue(String rule, String value) {
if (!gameRules.setValue(rule, value)) {
return false;
}
if (rule.equals("doDaylightCycle")) {
// inform clients about the daylight cycle change
getRawPlayers().forEach(GlowPlayer::sendTime);
} else if (rule.equals("reducedDebugInfo")) {
// inform clients about the debug info change
EntityStatusMessage message = new EntityStatusMessage(0,
gameRules.getBoolean("reducedDebugInfo")
? EntityStatusMessage.ENABLE_REDUCED_DEBUG_INFO
: EntityStatusMessage.DISABLE_REDUCED_DEBUG_INFO);
for (GlowPlayer player : getRawPlayers()) {
player.getSession().send(message);
}
}
return true;
}
@Override
public boolean isGameRule(String rule) {
return gameRules.isGameRule(rule);
}
@Override
public GlowWorldBorder getWorldBorder() {
return worldBorder;
}
public Map<String, CommandFunction> getFunctions() {
return functions;
}
@Override
public void spawnParticle(Particle particle, Location location, int count) {
spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count);
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count) {
spawnParticle(particle, x, y, z, count, null);
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, T data) {
spawnParticle(particle, location.getX(), location.getY(), location.getZ(), count, data);
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count,
T data) {
spawnParticle(particle, x, y, z, count, 0, 0, 0, data);
}
@Override
public void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ) {
spawnParticle(particle, location.getX(), location.getY(), location
.getZ(), count, offsetX, offsetY, offsetZ);
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, null);
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ, T data) {
spawnParticle(particle, location.getX(), location.getY(), location
.getZ(), count, offsetX, offsetY, offsetZ, data);
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ, T data) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, 1, data);
}
@Override
public void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ, double extra) {
spawnParticle(particle, location.getX(), location.getY(), location
.getZ(), count, offsetX, offsetY, offsetZ, extra);
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ, double extra) {
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, null);
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, double offsetX,
double offsetY, double offsetZ, double extra, T data) {
if (particle == null) {
throw new IllegalArgumentException("particle cannot be null!");
}
if (data != null && !particle.getDataType().isInstance(data)) {
throw new IllegalArgumentException(
"wrong data type " + data.getClass() + " should be " + particle.getDataType());
}
for (GlowPlayer player : getRawPlayers()) {
if (!player.getWorld().equals(this)) {
continue;
}
player.spawnParticle(particle, location, count, offsetX, offsetY, offsetZ, extra, data);
}
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count,
double offsetX, double offsetY, double offsetZ, double extra, T data) {
spawnParticle(particle, new Location(this, x, y, z), count, offsetX, offsetY, offsetZ,
extra, data);
}
public GameRuleManager getGameRuleMap() {
return gameRules;
}
@Override
public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
metadata.setMetadata(this, metadataKey, newMetadataValue);
}
////////////////////////////////////////////////////////////////////////////
// Metadata
@Override
public List<MetadataValue> getMetadata(String metadataKey) {
return metadata.getMetadata(this, metadataKey);
}
@Override
public boolean hasMetadata(String metadataKey) {
return metadata.hasMetadata(this, metadataKey);
}
@Override
public void removeMetadata(String metadataKey, Plugin owningPlugin) {
metadata.removeMetadata(this, metadataKey, owningPlugin);
}
@Override
public void sendPluginMessage(Plugin source, String channel, byte[] message) {
StandardMessenger.validatePluginMessage(server.getMessenger(), source, channel, message);
for (Player player : getRawPlayers()) {
player.sendPluginMessage(source, channel, message);
}
}
////////////////////////////////////////////////////////////////////////////
// Plugin messages
@Override
public Set<String> getListeningPluginChannels() {
HashSet<String> result = new HashSet<>();
for (Player player : getRawPlayers()) {
result.addAll(player.getListeningPluginChannels());
}
return result;
}
private void pulseTickMap() {
ItemTable itemTable = ItemTable.instance();
for (Location location : getTickMap()) {
GlowChunk chunk = (GlowChunk) location.getChunk();
if (!chunk.isLoaded()) {
continue;
}
int typeId = chunk.getType(
location.getBlockX() & 0xF, location.getBlockZ() & 0xF, location.getBlockY());
BlockType type = itemTable.getBlock(Material.getMaterial(typeId));
if (type == null) {
cancelPulse(location);
continue;
}
GlowBlock block = new GlowBlock(chunk, location.getBlockX(), location
.getBlockY(), location.getBlockZ());
Integer speed = type.getPulseTickSpeed(block);
boolean once = type.isPulseOnce(block);
if (speed == 0) {
continue;
}
if (worldAge % speed == 0) {
type.receivePulse(block);
if (once) {
cancelPulse(location);
}
}
}
}
public ConcurrentSet<Location> getTickMap() {
return tickMap;
}
public void requestPulse(GlowBlock block) {
requestPulse(block.getLocation());
}
public void requestPulse(Location location) {
tickMap.add(location);
}
public void cancelPulse(GlowBlock block) {
cancelPulse(block.getLocation());
}
public void cancelPulse(Location location) {
tickMap.remove(location);
}
@Override
public int hashCode() {
return getUID().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj == this;
}
/**
* The metadata store class for worlds.
*/
private static final class WorldMetadataStore extends MetadataStoreBase<World>
implements MetadataStore<World> {
@Override
protected String disambiguate(World subject, String metadataKey) {
return subject.getName() + ":" + metadataKey;
}
}
}