package net.glowstone; import lombok.ToString; import net.glowstone.block.GlowBlock; import net.glowstone.constants.GlowBiome; import net.glowstone.constants.GlowEffect; import net.glowstone.constants.GlowParticle; import net.glowstone.entity.*; import net.glowstone.entity.objects.GlowItem; import net.glowstone.generator.TreeGenerator; 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.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.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 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 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 = false; /** * 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 = false; /** * 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; /** * 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); chunks = new ChunkManager(this, storageProvider.getChunkIoService(), generator); 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(); difficulty = server.getDifficulty(); // 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(); } // begin loading spawn area spawnChunkLock = newChunkLock("spawn"); server.addWorld(this); server.getLogger().info("Preparing spawn for " + name + "..."); EventFactory.callEvent(new WorldInitEvent(this)); // 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 < 10 && !generator.canSpawn(this, spawnX, spawnZ); ++tries) { spawnX += random.nextInt(128) - 64; spawnZ += random.nextInt(128) - 64; } setSpawnLocation(spawnX, getHighestBlockYAt(spawnX, spawnZ), spawnZ); } } // load up chunks around the spawn location spawnChunkLock.clear(); if (keepSpawnLoaded) { 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); 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(); } } } } 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<GlowEntity> players = new LinkedList<>(); // 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(entity); } else { entity.pulse(); } } for (GlowEntity entity : players) { 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(); if (currentlyRaining && currentlyThundering) { if (random.nextDouble() < .01) { GlowChunk[] chunkList = chunks.getLoadedChunks(); if (chunkList.length > 0) { GlowChunk chunk = chunkList[random.nextInt(chunkList.length)]; int x = (chunk.getX() << 4) + random.nextInt(16); int z = (chunk.getZ() << 4) + random.nextInt(16); int y = getHighestBlockYAt(x, z); strikeLightning(new Location(this, x, y, z)); } } } } 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)); } @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; spawnLocation = new Location(this, x, y, z); 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; // 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; for (int x = centerX - radius; x <= centerX + radius; ++x) { for (int z = centerZ - radius; z <= centerZ + radius; ++z) { loadChunk(x, z); spawnChunkLock.acquire(new GlowChunk.Key(x, z)); } } } 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 GlowChunk.DEPTH; } @Override public int getSeaLevel() { return getMaxHeight() / 2; } @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(); final TreeGenerator generator = new TreeGenerator(blockStateDelegate); if (generator.generate(random, loc, type)) { final 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; } //////////////////////////////////////////////////////////////////////////// // 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); } //////////////////////////////////////////////////////////////////////////// // 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) { throw new UnsupportedOperationException("Not supported yet."); } @Override public double getHumidity(int x, int z) { throw new UnsupportedOperationException("Not supported yet."); } //////////////////////////////////////////////////////////////////////////// // 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); return dropItem(location, item); } @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); } } } @Override public void showParticle(Location loc, Particle particle, float offsetX, float offsetY, float offsetZ, float speed, int amount) { showParticle(loc, particle, null, offsetX, offsetY, offsetZ, speed, amount); } @Override public void showParticle(Location loc, Particle particle, MaterialData material, float offsetX, float offsetY, float offsetZ, float speed, int amount) { if (loc == null || particle == null) return; final double radiusSquared; if (GlowParticle.isLongDistance(particle)) { radiusSquared = 48 * 48; } else { radiusSquared = 16 * 16; } for (Player player : getRawPlayers()) { if (player.getLocation().distanceSquared(loc) <= radiusSquared) { player.showParticle(loc, particle, material, offsetX, offsetY, offsetZ, speed, amount); } } } //////////////////////////////////////////////////////////////////////////// // 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(); } catch (IOException e) { server.getLogger().severe("Could not save metadata 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(); } 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); } 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; } }