| 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; |
| |
| /** |
| * Contains how regular blocks should be pulsed. |
| */ |
| private final Map tickMap = new HashMap<>(); |
| |
| /** |
| * 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; |
| } |
| |
| private void pulseTickMap() { |
| ItemTable itemTable = ItemTable.instance(); |
| Map map = getTickMap(); |
| for (Map.Entry entry : map.entrySet()) { |
| if (worldAge % entry.getValue() == 0) { |
| GlowBlock block = this.getBlockAt(entry.getKey()); |
| BlockType notifyType = itemTable.getBlock(block.getTypeId()); |
| if (notifyType != null) |
| notifyType.recievePulse(block); |
| } |
| |
| private Map<Location, Integer> getTickMap() { |
| return new HashMap<>(tickMap); |
| } |
| |
| public void requestPulse(GlowBlock block, int tickRate) { |
| Map<Location, Integer> map = getTickMap(); |
| Location target = block.getLocation(); |
| |
| for (Location location : map.keySet()) { |
| if (target.equals(location)) { |
| if (tickRate > 0) |
| tickMap.put(location, tickRate); |
| else |
| tickMap.remove(location); |
| return; |
| } |
| } |
| |
| if (tickRate > 0) |
| tickMap.put(target, tickRate); |
| } |
| |
| public void cancelPulse(GlowBlock block) { |
| requestPulse(block, 0); |
| } |
| } |