package net.glowstone;
import lombok.ToString;
import net.glowstone.GlowChunk.ChunkSection;
import net.glowstone.block.GlowBlock;
import net.glowstone.block.ItemTable;
import net.glowstone.block.blocktype.BlockType;
import net.glowstone.constants.*;
import net.glowstone.entity.*;
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.io.anvil.AnvilWorldStorageProvider;
import net.glowstone.net.message.play.entity.EntityStatusMessage;
import net.glowstone.net.message.play.player.ServerDifficultyMessage;
import net.glowstone.util.BlockStateDelegate;
import net.glowstone.util.GameRuleManager;
import org.bukkit.*;
import org.bukkit.block.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.*;
import org.bukkit.event.weather.LightningStrikeEvent;
import org.bukkit.event.weather.ThunderChangeEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.bukkit.event.world.*;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.inventory.ItemStack;
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.Vector;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
/**
* A class which represents the in-game world.
* @author Graham Edgecombe
*/
@ToString(of = "name")
public final class GlowWorld implements World {
/**
* 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;
}
}
/**
* The metadata store for world objects.
*/
private static final MetadataStore<World> metadata = new WorldMetadataStore();
/**
* The length in ticks of one Minecraft day.
*/
public static final long DAY_LENGTH = 24000;
/**
* The length in ticks between autosaves (5 minutes).
*/
private static final int AUTOSAVE_TIME = 20 * 60 * 5;
/**
* The maximum height of ocean water.
*/
private static final int SEA_LEVEL = 64;
/**
* 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;
/**
* A lock kept on the spawn chunks.
*/
private final ChunkManager.ChunkLock spawnChunkLock;
/**
* 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();
/**
* This world's Random instance.
*/
private final Random random = new Random();
/**
* 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;
/**
* The world type.
*/
private final WorldType worldType;
/**
* Whether structure generation is enabled.
*/
private final boolean generateStructures;
/**
* The world seed.
*/
private final long seed;
/**
* 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 = false;
/**
* 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 = 0;
/**
* 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 = 0;
/**
* The rain density on the current world tick.
*/
private float currentRainDensity = 0;
/**
* The sky darkness on the current world tick.
*/
private float currentSkyDarkness = 0;
/**
* The age of the world, in ticks.
*/
private long worldAge = 0;
/**
* The current world time.
*/
private long time = 0;
/**
* 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 = Difficulty.PEACEFUL;
/**
* Ticks between when various types of entities are spawned.
*/
private long ticksPerAnimal, ticksPerMonster;
/**
* Per-chunk spawn limits on various types of entities.
*/
private int monsterLimit, animalLimit, waterAnimalLimit, ambientLimit;
/**
* Contains how regular blocks should be pulsed.
*/
private final Map tickMap = new HashMap<>();
private Map<Integer, GlowStructure> structures;
/**
* The maximum height at which players may place blocks.
*/
private int maxBuildHeight;
private Set<GlowChunk.Key> activeChunksSet = new HashSet<GlowChunk.Key>();
/**
* 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) {
this.server = server;
// set up values from WorldCreator
name = creator.name();
environment = creator.environment();
worldType = creator.type();
generateStructures = creator.generateStructures();
final ChunkGenerator generator = creator.generator();
storageProvider = new AnvilWorldStorageProvider(new File(server.getWorldContainer(), name));
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();
// read in world data
WorldFinalValues values = null;
try {
values = storageProvider.getMetadataService().readWorldData();
} catch (IOException e) {
server.getLogger().log(Level.SEVERE, "Error reading world for creation", e);
}
if (values != null) {
if (values.getSeed() == 0L) {
this.seed = creator.seed();
} else {
this.seed = values.getSeed();
}
this.uid = values.getUuid();
} else {
this.seed = creator.seed();
this.uid = UUID.randomUUID();
}
chunks = new ChunkManager(this, storageProvider.getChunkIoService(), generator);
try {
structures = storageProvider.getStructureDataService().readStructuresData();
} catch (IOException e) {
server.getLogger().log(Level.SEVERE, "Error reading structure data for world " + getName(), e);
}
server.addWorld(this);
server.getLogger().info("Preparing spawn for " + name + "...");
EventFactory.callEvent(new WorldInitEvent(this));
if (keepSpawnLoaded) {
spawnChunkLock = newChunkLock("spawn");
} else {
spawnChunkLock = null;
}
// determine the spawn location if we need to
if (spawnLocation == null) {
// no location loaded, look for fixed spawn
spawnLocation = generator.getFixedSpawnLocation(this, random);
if (spawnLocation == null) {
// determine a location randomly
int spawnX = random.nextInt(128) - 64, spawnZ = random.nextInt(128) - 64;
GlowChunk chunk = getChunkAt(spawnX >> 4, spawnZ >> 4);
//GlowServer.logger.info("determining spawn: " + chunk.getX() + " " + chunk.getZ());
chunk.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 += random.nextInt(128) - 64;
spawnZ += random.nextInt(128) - 64;
}
setSpawnLocation(spawnX, getHighestBlockYAt(spawnX, spawnZ), spawnZ);
}
} else if (keepSpawnLoaded) {
setKeepSpawnInMemory(keepSpawnLoaded);
}
server.getLogger().info("Preparing spawn for " + name + ": done");
EventFactory.callEvent(new WorldLoadEvent(this));
}
////////////////////////////////////////////////////////////////////////////
// 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.
* @return The ChunkLock.
*/
public ChunkManager.ChunkLock newChunkLock(String desc) {
return new ChunkManager.ChunkLock(chunks, name + ": " + desc);
}
/**
* Updates all the entities within this world.
*/
public void pulse() {
List<GlowEntity> temp = new ArrayList<>(entities.getAll());
List<GlowPlayer> players = new LinkedList<>();
activeChunksSet.clear();
// We should pulse our tickmap, so blocks get updated.
this.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 : temp) {
if (entity instanceof GlowPlayer) {
players.add((GlowPlayer) entity);
// build a set of chunks around each player in this world, the
// server view distance is taken here
final int radius = server.getViewDistance();
final Location playerLocation = entity.getLocation();
if (playerLocation.getWorld() == this) {
final int cx = playerLocation.getBlockX() >> 4;
final 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(new GlowChunk.Key(x, z));
}
}
}
}
} else {
entity.pulse();
}
}
for (GlowChunk.Key key : activeChunksSet) {
final int cx = key.getX();
final int cz = key.getZ();
// check the chunk is loaded
if (isChunkLoaded(cx, cz)) {
final GlowChunk chunk = getChunkAt(cx, cz);
// thunder
if (environment == Environment.NORMAL &&
currentlyRaining && currentlyThundering) {
if (random.nextInt(25000) == 0) {
final int n = random.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
final BoundingBox searchBox = BoundingBox.fromPositionAndSize(new Vector(x, y, z), new Vector(0, 0, 0));
final Vector vec = new Vector(3, 3, 3);
final Vector vec2 = new Vector(0, getMaxHeight(), 0);
searchBox.minCorner.subtract(vec);
searchBox.maxCorner.add(vec).add(vec2);
final List<LivingEntity> livingEntities = new LinkedList<>();
for (Entity entity : getEntityManager().getEntitiesInside(searchBox, null)) {
if (entity instanceof LivingEntity && !entity.isDead()) {
// make sure entity can see sky
final 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
final LivingEntity entity = livingEntities.get(random.nextInt(livingEntities.size()));
// re-target lightning on this living entity
final 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));
}
}
}
// block ticking
// we will choose 3 blocks per chunk's section
final ChunkSection[] sections = chunk.getSections();
for (int i = 0; i < sections.length; i++) {
final ChunkSection section = sections[i];
if (section != null) {
for (int j = 0; j < 3; j++) {
final int n = random.nextInt();
final int x = n & 0xF;
final int z = n >> 8 & 0xF;
final int y = n >> 16 & 0xF;
final int type = section.types[(y << 8) | (z << 4) | x] >> 4;
if (type != 0) { // filter air blocks
final 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));
}
}
}
}
}
}
}
for (GlowEntity entity : players) {
if (entity != null) {
entity.pulse();
}
}
for (GlowEntity entity : temp) {
entity.reset();
}
// Tick the world age and time of day
// Modulus by 24000, the tick length of a day
worldAge++;
if (gameRules.getBoolean("doDaylightCycle")) {
time = (time + 1) % DAY_LENGTH;
}
if (worldAge % (30 * 20) == 0) {
// Only send the time every so often; clients are smart.
for (GlowPlayer player : getRawPlayers()) {
player.sendTime();
}
}
// only tick weather in a NORMAL world
if (environment == Environment.NORMAL) {
if (--rainingTicks <= 0) {
setStorm(!currentlyRaining);
}
if (--thunderingTicks <= 0) {
setThundering(!currentlyThundering);
}
updateWeather();
}
// 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) {
for (GlowPlayer player : players) {
if (player.isSleeping()) {
player.leaveBed(true);
}
}
} else { // otherwise check whether everyone is asleep
boolean allSleeping = true;
for (GlowPlayer player : players) {
if (!(player.isSleeping() && player.getSleepTicks() >= 100) && !player.isSleepingIgnored()) {
allSleeping = false;
break;
}
}
if (allSleeping && gameRules.getBoolean("doDaylightCycle")) {
worldAge = (worldAge / DAY_LENGTH + 1) * DAY_LENGTH;
time = 0;
for (GlowPlayer player : players) {
player.sendTime();
if (player.isSleeping()) {
player.leaveBed(true);
}
}
setStorm(false);
setThundering(false);
}
}
}
if (--saveTimer <= 0) {
saveTimer = AUTOSAVE_TIME;
chunks.unloadOldChunks();
if (autosave) {
save(true);
}
}
}
/**
* 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 1;
}
/**
* 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<Player>(entities.getAll(GlowPlayer.class));
}
/**
* Returns a list of entities within a bounding box centered around a Location.
*
* 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<Entity>(entities.getAll());
}
@Override
public List<LivingEntity> getLivingEntities() {
List<LivingEntity> result = new LinkedList<>();
for (Entity e : entities.getAll()) {
if (e instanceof GlowLivingEntity) result.add((GlowLivingEntity) e);
}
return result;
}
@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) {
ArrayList<T> result = new ArrayList<>();
for (Entity e : entities.getAll()) {
if (cls.isAssignableFrom(e.getClass())) {
result.add((T) e);
}
}
return result;
}
@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(int x, int y, int z) {
Location oldSpawn = spawnLocation;
Location newSpawn = new Location(this, x, y, z);
if (newSpawn.equals(oldSpawn)) {
return false;
}
spawnLocation = newSpawn;
setKeepSpawnInMemory(keepSpawnLoaded);
EventFactory.callEvent(new SpawnChangeEvent(this, oldSpawn));
return true;
}
@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;
if (spawnChunkLock != null) {
// update the chunk lock as needed
spawnChunkLock.clear();
if (keepLoaded) {
int centerX = spawnLocation.getBlockX() >> 4;
int centerZ = spawnLocation.getBlockZ() >> 4;
int radius = 4 * server.getViewDistance() / 3;
long loadTime = System.currentTimeMillis();
int total = (radius * 2 + 1) * (radius * 2 + 1), current = 0;
for (int x = centerX - radius; x <= centerX + radius; ++x) {
for (int z = centerZ - radius; z <= centerZ + radius; ++z) {
++current;
loadChunk(x, z);
if (populateAnchoredChunks) {
getChunkManager().forcePopulation(x, z);
}
spawnChunkLock.acquire(new GlowChunk.Key(x, z));
if (System.currentTimeMillis() >= loadTime + 1000) {
int progress = 100 * current / total;
GlowServer.logger.info("Preparing spawn for " + name + ": " + progress + "%");
loadTime = System.currentTimeMillis();
}
}
}
} else {
// attempt to immediately unload the spawn
chunks.unloadOldChunks();
}
}
}
@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.getValue());
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() {
return SEA_LEVEL;
}
@Override
public WorldType getWorldType() {
return worldType;
}
@Override
public boolean canGenerateStructures() {
return generateStructures;
}
////////////////////////////////////////////////////////////////////////////
// force-save
@Override
public void save() {
save(false);
}
public void save(boolean async) {
EventFactory.callEvent(new WorldSaveEvent(this));
// save metadata
writeWorldData(async);
// save chunks
maybeAsync(async, new Runnable() {
@Override
public void run() {
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) {
final BlockStateDelegate blockStateDelegate = new BlockStateDelegate();
if (GlowTree.newInstance(type, random, loc, blockStateDelegate).generate()) {
final List<BlockState> blockStates = new ArrayList<BlockState>(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 GlowBlock getBlockAt(int x, int y, int z) {
return new GlowBlock(getChunkAt(x >> 4, z >> 4), x, y & 0xff, z);
}
@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(int x, int z) {
return getChunkAt(x >> 4, z >> 4).getHeight(x & 0xf, z & 0xf);
}
@Override
public GlowChunk getChunkAt(int x, int z) {
return chunks.getChunk(x, z);
}
////////////////////////////////////////////////////////////////////////////
// get block, chunk, id, highest with locations
@Override
public GlowBlock getBlockAt(Location location) {
return getBlockAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
@Override
public int getBlockTypeIdAt(Location location) {
return getBlockTypeIdAt(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
@Override
public int getHighestBlockYAt(Location location) {
return getHighestBlockYAt(location.getBlockX(), location.getBlockZ());
}
@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());
}
@Override
public Chunk getChunkAt(Location location) {
return getChunkAt(location.getBlockX() >> 4, location.getBlockZ() >> 4);
}
@Override
public Chunk getChunkAt(Block block) {
return getChunkAt(block.getX() >> 4, block.getZ() >> 4);
}
@Override
public void getChunkAtAsync(int x, int z, ChunkLoadCallback cb) {
}
@Override
public void getChunkAtAsync(Location location, ChunkLoadCallback cb) {
}
@Override
public void getChunkAtAsync(Block block, ChunkLoadCallback 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) {
getChunkAt(x, z).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(final int x, final int z, final boolean safe) {
if (safe && isChunkInUse(x, z)) return false;
server.getScheduler().runTask(null, new Runnable() {
@Override
public void run() {
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;
}
GlowChunk.Key key = new GlowChunk.Key(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 GlowChunkSnapshot.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) {
getChunkAt(x >> 4, z >> 4).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 {
GlowEntity entity = null;
if (TNTPrimed.class.isAssignableFrom(clazz)) {
entity = new GlowTNTPrimed(location, null);
}
if (entity != null) {
@SuppressWarnings("unchecked")
T result = (T) entity;
return result;
}
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public GlowItem dropItem(Location location, ItemStack item) {
return new GlowItem(location, item);
}
@Override
public GlowItem dropItemNaturally(Location location, ItemStack item) {
double xs = random.nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
double ys = random.nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
double zs = random.nextFloat() * 0.7F + (1.0F - 0.7F) * 0.5D;
location = location.clone().add(xs, ys, zs);
GlowItem dropItem = new GlowItem(location, item);
dropItem.setVelocity(new Vector(0, 0.1F, 0));
return dropItem;
}
@Override
public Arrow spawnArrow(Location location, Vector velocity, float speed, float spread) {
Arrow arrow = spawn(location, Arrow.class);
// Transformative magic
Vector randVec = new Vector(random.nextGaussian(), random.nextGaussian(), random.nextGaussian());
randVec.multiply(0.0075 * (double) 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.setVelocity(velocity);
return arrow;
}
@Override
public FallingBlock spawnFallingBlock(Location location, Material material, byte data) throws IllegalArgumentException {
return null;
}
@Override
public FallingBlock spawnFallingBlock(Location location, int blockId, byte blockData) throws IllegalArgumentException {
return null;
}
@Override
public Entity spawnEntity(Location loc, EntityType type) {
return spawn(loc, type.getEntityClass());
}
@Override
@Deprecated
public LivingEntity spawnCreature(Location loc, EntityType type) {
return (LivingEntity) spawn(loc, type.getEntityClass());
}
@Override
@Deprecated
public LivingEntity spawnCreature(Location loc, CreatureType type) {
return (LivingEntity) spawn(loc, type.getEntityClass());
}
private GlowLightningStrike strikeLightningFireEvent(final Location loc, final boolean effect) {
final GlowLightningStrike strike = new GlowLightningStrike(loc, effect, random);
final 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);
}
@Override
public GlowLightningStrike strikeLightningEffect(Location loc) {
return strikeLightningFireEvent(loc, true);
}
////////////////////////////////////////////////////////////////////////////
// Time
@Override
public long getTime() {
return time;
}
@Override
public void setTime(long time) {
this.time = (time % DAY_LENGTH + DAY_LENGTH) % DAY_LENGTH;
for (GlowPlayer player : getRawPlayers()) {
player.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(random.nextInt(12000) + 12000);
} else {
setWeatherDuration(random.nextInt(168000) + 12000);
}
// update players
if (previouslyRaining != currentlyRaining) {
for (GlowPlayer player : getRawPlayers()) {
player.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(random.nextInt(12000) + 3600);
} else {
setThunderDuration(random.nextInt(168000) + 12000);
}
}
@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() {
final float previousRainDensity = currentRainDensity;
final float previousSkyDarkness = currentSkyDarkness;
final float rainDensityModifier = currentlyRaining ? .01F : -.01F;
final 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) {
for (GlowPlayer player : getRawPlayers()) {
player.sendRainDensity();
}
}
if (previousSkyDarkness != currentSkyDarkness) {
for (GlowPlayer player : getRawPlayers()) {
player.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) {
final int radiusSquared = radius * radius;
for (Player player : getRawPlayers()) {
if (player.getLocation().distanceSquared(location) <= radiusSquared) {
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);
}
public void playEffectExceptTo(Location location, Effect effect, int data, int radius, Player exclude) {
final int radiusSquared = radius * radius;
for (Player player : getRawPlayers()) {
if (!player.equals(exclude) && player.getLocation().distanceSquared(location) <= radiusSquared) {
player.playEffect(location, effect, data);
}
}
}
@Override
public void playSound(Location location, Sound sound, float volume, float pitch) {
if (location == null || sound == null) return;
final double radiusSquared = Math.pow(volume * 16, 2);
for (Player player : getRawPlayers()) {
if (player.getLocation().distanceSquared(location) <= radiusSquared) {
player.playSound(location, sound, volume, pitch);
}
}
}
private void playEffect_(Location location, Effect effect, int data) { // fix name collision
playEffect(location, effect, data);
}
private final Spigot spigot = new Spigot() {
@Override
public void playEffect(Location location, Effect effect) {
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 Spigot spigot() {
return spigot;
}
//@Override
public void showParticle(Location loc, Effect particle, float offsetX, float offsetY, float offsetZ, float speed, int amount) {
final int radius;
if (GlowParticle.isLongDistance(particle)) {
radius = 48;
} else {
radius = 16;
}
showParticle(loc, particle, particle.getId(), 0, offsetX, offsetY, offsetZ, speed, amount, radius);
}
//@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;
final double radiusSquared = radius * radius;
for (Player player : getRawPlayers()) {
if (player.getLocation().distanceSquared(loc) <= radiusSquared) {
player.spigot().playEffect(loc, particle, id, data, offsetX, offsetY, offsetZ, speed, amount, radius);
}
}
}
////////////////////////////////////////////////////////////////////////////
// Level data write
/**
* Save the world data using the metadata service.
* @param async Whether to write asynchronously.
*/
private void writeWorldData(boolean async) {
maybeAsync(async, new Runnable() {
@Override
public void run() {
try {
storageProvider.getMetadataService().writeWorldData();
storageProvider.getScoreboardIoService().save();
} catch (IOException e) {
server.getLogger().severe("Could not save metadata for world: " + getName());
e.printStackTrace();
}
try {
storageProvider.getStructureDataService().writeStructuresData(structures);
} catch (IOException e) {
server.getLogger().severe("Could not save structures data for world: " + getName());
e.printStackTrace();
}
}
});
}
/**
* 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() {
EventFactory.callEvent(new WorldUnloadEvent(this));
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();
}
////////////////////////////////////////////////////////////////////////////
// Game rules
@Override
public String[] getGameRules() {
return gameRules.getKeys();
}
@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
for (GlowPlayer player : getRawPlayers()) {
player.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 WorldBorder getWorldBorder() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
public GameRuleManager getGameRuleMap() {
return gameRules;
}
////////////////////////////////////////////////////////////////////////////
// Metadata
@Override
public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
metadata.setMetadata(this, metadataKey, newMetadataValue);
}
@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);
}
////////////////////////////////////////////////////////////////////////////
// Plugin messages
@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);
}
}
@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();
Map<Location, Long> map = getTickMap();
for (Map.Entry<Location, Long> entry : map.entrySet()) {
if (worldAge % entry.getValue() == 0) {
GlowBlock block = this.getBlockAt(entry.getKey());
BlockType notifyType = itemTable.getBlock(block.getTypeId());
if (notifyType != null)
notifyType.receivePulse(block);
}
}
}
private Map<Location, Long> getTickMap() {
return new HashMap<>(tickMap);
}
/**
* Calling this method will request that the block is ticked on the next iteration
* that applies to the specified tick rate.
*/
public void requestPulse(GlowBlock block, long tickRate) {
Location target = block.getLocation();
if (tickRate > 0)
tickMap.put(target, tickRate);
else if (tickMap.containsKey(target))
tickMap.remove(target);
}
public void cancelPulse(GlowBlock block) {
requestPulse(block, 0);
}
}