/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.world; import static com.google.common.base.Preconditions.checkNotNull; import com.flowpowered.math.vector.Vector3d; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.lanternpowered.server.config.GlobalConfig; import org.lanternpowered.server.config.world.WorldConfig; import org.lanternpowered.server.data.io.ScoreboardIO; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.game.LanternGame; import org.lanternpowered.server.util.ThreadHelper; import org.lanternpowered.server.world.LanternWorldPropertiesIO.LevelData; import org.spongepowered.api.Sponge; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.cause.Cause; import org.spongepowered.api.event.world.LoadWorldEvent; import org.spongepowered.api.scoreboard.Scoreboard; import org.spongepowered.api.util.Functional; import org.spongepowered.api.util.GuavaCollectors; import org.spongepowered.api.util.Tuple; import org.spongepowered.api.world.GeneratorTypes; import org.spongepowered.api.world.World; import org.spongepowered.api.world.WorldArchetype; import org.spongepowered.api.world.WorldArchetypes; import org.spongepowered.api.world.storage.WorldProperties; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Phaser; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.annotation.Nullable; public final class LanternWorldManager { // The counter for the executor threads private final AtomicInteger counter = new AtomicInteger(); // The executor for async world manager operations private final ExecutorService executor = Executors.newCachedThreadPool( runnable -> new Thread(runnable, "worlds-" + this.counter.getAndIncrement())); // The name of the world configs static final String WORLD_CONFIG = "world.conf"; // The size of the dimension map static final int DIMENSION_MAP_SIZE = Long.SIZE << 4; // A lookup entry used to store world references private static class WorldLookupEntry { // The world properties public final LanternWorldProperties properties; // The folder where all the world files are stored public final Path folder; // The dimension id of the world public final int dimensionId; // The reference to the world instance @Nullable public volatile LanternWorld world; WorldLookupEntry(LanternWorldProperties properties, Path folder, int dimensionId) { this.dimensionId = dimensionId; this.properties = properties; this.folder = folder; } } // A map with all the world threads private final Map<LanternWorld, Thread> worldThreads = Maps.newConcurrentMap(); // The world entries indexed by the name private final Map<LanternWorldProperties, WorldLookupEntry> worldByProperties = Maps.newConcurrentMap(); // The world entries indexed by the name private final Map<String, WorldLookupEntry> worldByName = Maps.newConcurrentMap(); // The world entries indexed by the unique ids private final Map<UUID, WorldLookupEntry> worldByUUID = Maps.newConcurrentMap(); // The world entries indexed by the dimension ids private final Map<Integer, WorldLookupEntry> worldByDimensionId = Maps.newConcurrentMap(); // The map of all the dimension ids private BitSet dimensionMap; // The directory of the root world private final Path rootWorldDirectory; // The global configuration file private final GlobalConfig globalConfig; // The game instance private final LanternGame game; // The phasers to synchronize the world threads private final Phaser tickBegin = new Phaser(1); private final Phaser tickEnd = new Phaser(1); /** * Creates a new world manager. * * @param game The game instance * @param rootWorldDirectory The root world directory */ public LanternWorldManager(LanternGame game, Path rootWorldDirectory) { this.rootWorldDirectory = rootWorldDirectory; this.globalConfig = game.getGlobalConfig(); this.game = game; } /** * Gets a loaded {@link World} by name, if it exists. * * @param worldName name to lookup * @return the world, if found */ public Optional<World> getWorld(String worldName) { checkNotNull(worldName, "worldName"); return this.worldByName.containsKey(worldName) ? Optional.ofNullable(this.worldByName.get(worldName).world) : Optional.empty(); } /** * Gets all currently loaded {@link World}s. * * @return a collection of loaded worlds */ public Collection<World> getWorlds() { return this.worldByUUID.values().stream().map(e -> e.world).collect(GuavaCollectors.toImmutableList()); } /** * Gets a loaded {@link World} by its unique id ({@link UUID}), if it * exists. * * @param uniqueId uuid to lookup * @return the world, if found */ public Optional<World> getWorld(UUID uniqueId) { checkNotNull(uniqueId, "uniqueId"); return this.worldByUUID.containsKey(uniqueId) ? Optional.ofNullable(this.worldByUUID.get(uniqueId).world) : Optional.empty(); } /** * Gets the properties of all worlds, loaded or otherwise. * * @return a collection of world properties */ public Collection<WorldProperties> getAllWorldProperties() { return this.worldByUUID.values().stream().map(e -> e.properties).collect(GuavaCollectors.toImmutableList()); } /** * Gets the properties of all unloaded worlds. * * @return a collection of world properties */ public Collection<WorldProperties> getUnloadedWorlds() { return this.worldByUUID.values().stream().filter(e -> e.world == null).map(e -> e.properties).collect(GuavaCollectors.toImmutableList()); } /** * Gets the {@link WorldProperties} of a world. If a world with the given * name is loaded then this is equivalent to calling * {@link World#getProperties()}. However, if no loaded world is found then * an attempt will be made to match unloaded worlds. * * @param worldName the name to lookup * @return the world properties, if found */ public Optional<WorldProperties> getWorldProperties(String worldName) { checkNotNull(worldName, "worldName"); WorldLookupEntry entry = this.worldByName.get(worldName); return entry != null ? Optional.of(entry.properties) : Optional.empty(); } /** * Gets the {@link WorldProperties} of a world. If a world with the given * UUID is loaded then this is equivalent to calling * {@link World#getProperties()}. However, if no loaded world is found then * an attempt will be made to match unloaded worlds. * * @param uniqueId the uuid to lookup * @return the world properties, if found */ public Optional<WorldProperties> getWorldProperties(UUID uniqueId) { checkNotNull(uniqueId, "uniqueId"); WorldLookupEntry entry = this.worldByUUID.get(uniqueId); return entry != null ? Optional.of(entry.properties) : Optional.empty(); } /** * Gets the {@link WorldProperties} of a world. If a world with the given * UUID is loaded then this is equivalent to calling * {@link World#getProperties()}. However, if no loaded world is found then * an attempt will be made to match unloaded worlds. * * @param dimensionId the uuid to lookup * @return the world properties, if found */ public Optional<WorldProperties> getWorldProperties(int dimensionId) { WorldLookupEntry entry = this.worldByDimensionId.get(dimensionId); return entry != null ? Optional.of(entry.properties) : Optional.empty(); } public Optional<Integer> getWorldDimensionId(UUID uniqueId) { checkNotNull(uniqueId, "uniqueId"); WorldLookupEntry entry = this.worldByUUID.get(uniqueId); return entry != null ? Optional.of(entry.dimensionId) : Optional.empty(); } /** * Gets the properties of default world. * * @return the world properties */ public Optional<WorldProperties> getDefaultWorld() { WorldLookupEntry entry = this.worldByDimensionId.get(0); // Can be empty if the properties aren't loaded yet return entry != null ? Optional.of(entry.properties) : Optional.empty(); } /** * Unloads a {@link World}, if there are any connected players in the given * world then no operation will occur. * * <p>A world which is unloaded will be removed from memory. However if it * is still enabled according to {@link WorldProperties#isEnabled()} then it * will be loaded again if the server is restarted or an attempt is made by * a plugin to transfer an entity to the world using * {@link org.spongepowered.api.entity.Entity#transferToWorld(World, Vector3d)}.</p> * * @param world the world to unload * @return whether the operation was successful */ public boolean unloadWorld(World world) { checkNotNull(world, "world"); final LanternWorld world0 = (LanternWorld) world; // We cannot unload the world if there are // still players active if (!world0.getPlayers().isEmpty()) { return false; } // Post the unload world event this.game.getEventManager().post(SpongeEventFactory.createUnloadWorldEvent( Cause.source(this.game.getMinecraftPlugin()).build(), world)); // Save all the world data world0.shutdown(); // Remove the tick task this.removeWorldTask(world0); // Get the lookup entry and properties to remove all the references final LanternWorldProperties properties = world0.getProperties(); final WorldLookupEntry entry = this.worldByProperties.get(properties); properties.setWorld(null); entry.world = null; // Save the world properties this.saveWorldProperties(properties); try { ScoreboardIO.write(entry.folder, world0.getScoreboard()); } catch (IOException e) { Lantern.getLogger().warn("An error occurred while saving the scoreboard data.", e); } return true; } /** * Creates a world copy asynchronously using the new name given and returns * the new world properties if the copy was possible. * * <p>If the world is already loaded then the following will occur:</p> * * <ul> * <li>World is saved.</li> * <li>World saving is disabled.</li> * <li>World is copied. </li> * <li>World saving is enabled.</li> * </ul> * * @param worldProperties The world properties to copy * @param copyName The name for copied world * @return An {@link Optional} containing the properties of the new world * instance, if the copy was successful */ public CompletableFuture<Optional<WorldProperties>> copyWorld(WorldProperties worldProperties, String copyName) { checkNotNull(worldProperties, "worldProperties"); checkNotNull(copyName, "copyName"); return Functional.asyncFailableFuture(() -> { // Get the new dimension id final int dimensionId = this.getNextFreeDimensionId(); // TODO: Save the world if loaded // TODO: Block world saving // Get the lookup entry final WorldLookupEntry entry = this.worldByProperties.get(worldProperties); // The folder of the new world final Path targetFolder = this.getWorldFolder(copyName, dimensionId); // The folder of the original world final Path folder = entry.folder; if (Files.exists(targetFolder) && Files.list(targetFolder).count() > 0) { this.game.getLogger().error("The copy world folder already exists and it isn't empty!"); return Optional.empty(); } // Save the changes once more to make sure that they will be saved this.saveWorldProperties(worldProperties); // Copy the world folder final String folderPath = folder.toFile().getAbsolutePath(); try { Files.walkFileTree(folder, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { final Path dstPath = targetFolder.resolve(path.toFile() .getAbsolutePath().substring(folderPath.length())); Files.copy(path, dstPath, StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } }); } catch (IOException e) { this.game.getLogger().error("Failed to copy the world folder of {}: {} to {}", copyName, folder, targetFolder, e); return Optional.empty(); } final WorldConfigResult result = this.getOrCreateWorldConfig(copyName); // Copy the settings result.config.copyFrom(entry.properties.getConfig()); result.config.save(); final LevelData levelData; final LanternWorldProperties properties; try { levelData = LanternWorldPropertiesIO.read(folder, copyName, null); properties = LanternWorldPropertiesIO.convert(levelData, result.config, false); } catch (IOException e) { this.game.getLogger().error("Unable to open the copied world properties of {}", copyName, e); return Optional.empty(); } final LevelData newData = new LevelData(levelData.worldName, levelData.uniqueId, levelData.worldData, levelData.spongeWorldData, dimensionId, null); // Store the new world this.addUpdatedWorldProperties(properties, targetFolder, dimensionId); // Save the changes once more to make sure that they will be saved LanternWorldPropertiesIO.write(targetFolder, newData); return Optional.of(properties); }, this.executor); } /** * Renames an unloaded world. * * @param worldProperties The world properties to rename * @param newName The name that should be used as a replacement for the * current world name * @return An {@link Optional} containing the new {@link WorldProperties} * if the rename was successful */ public Optional<WorldProperties> renameWorld(WorldProperties worldProperties, String newName) { checkNotNull(worldProperties, "worldProperties"); checkNotNull(newName, "newName"); // There already exists a world with that name if (this.worldByName.containsKey(newName)) { return Optional.empty(); } final WorldLookupEntry entry = this.worldByProperties.get(worldProperties); // You cannot rename a loaded world if (entry.world != null || this.getWorld(worldProperties.getWorldName()).isPresent()) { return Optional.empty(); } final LanternWorldProperties worldProperties0 = (LanternWorldProperties) worldProperties; this.worldByName.put(newName, entry); this.worldByName.remove(worldProperties0.getWorldName()); worldProperties0.setName(newName); // Save the changes once more to make sure that they will be saved this.saveWorldProperties(worldProperties0); return Optional.of(worldProperties0); } /** * Deletes the provided world's files asynchronously from the disk. * * @param worldProperties the world properties to delete * @return true if the deletion was successful */ public CompletableFuture<Boolean> deleteWorld(WorldProperties worldProperties) { checkNotNull(worldProperties, "worldProperties"); return Functional.asyncFailableFuture(() -> { final WorldLookupEntry entry = this.worldByProperties.get(worldProperties); if (entry.world != null) { return false; } final boolean[] flag = { true }; Files.walkFileTree(entry.folder, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { try { Files.delete(path); } catch (IOException e) { game.getLogger().error("Unable to delete the file {} of world {}", path.toFile().getAbsolutePath(), worldProperties.getWorldName(), e); flag[0] = false; } return FileVisitResult.CONTINUE; } }); this.worldByName.remove(worldProperties.getWorldName()); this.worldByDimensionId.remove(entry.dimensionId); this.worldByProperties.remove(worldProperties); this.worldByUUID.remove(worldProperties.getUniqueId()); this.dimensionMap.clear(entry.dimensionId); Files.delete(entry.folder); return flag[0]; }, this.executor); } /** * Persists the given {@link WorldProperties} to the world storage for it, * updating any modified values. * * @param worldProperties the world properties to save * @return true if the save was successful */ public boolean saveWorldProperties(WorldProperties worldProperties) { checkNotNull(worldProperties, "worldProperties"); final WorldLookupEntry entry = this.worldByProperties.get(worldProperties); checkNotNull(entry, "entry"); final LanternWorldProperties worldProperties0 = (LanternWorldProperties) worldProperties; final BitSet dimensionMap = entry.dimensionId == 0 ? (BitSet) this.dimensionMap.clone() : null; try { LevelData levelData = LanternWorldPropertiesIO.convert(worldProperties0, entry.dimensionId, dimensionMap); LanternWorldPropertiesIO.write(entry.folder, levelData); worldProperties0.getConfig().save(); } catch (IOException e) { this.game.getLogger().error("Unable to save the world properties of {}: {}", worldProperties.getWorldName(), e.getMessage(), e); return false; } return true; } /** * Creates a new world from the given {@link WorldArchetype}. For the * creation of the WorldCreationSettings please see * {@link org.spongepowered.api.world.WorldArchetype.Builder}. * * <p>If the world already exists then the existing {@link WorldProperties} * are returned else a new world is created and the new WorldProperties * returned.</p> * * <p>Although the world is created it is not loaded at this time. Please * see one of the following methods for loading the world.</p> * * <ul> <li>{@link #loadWorld(String)}</li> <li>{@link #loadWorld(UUID)} * </li> <li>{@link #loadWorld(WorldProperties)}</li> </ul> * * @param worldArchetype The world archetype for creation * @return The new or existing world properties, if creation was successful */ public WorldProperties createWorldProperties(String folderName, WorldArchetype worldArchetype) throws IOException { checkNotNull(worldArchetype, "worldArchetype"); WorldLookupEntry entry = this.worldByName.get(worldArchetype.getName()); if (entry != null) { return entry.properties; } // Get the next dimension id final int dimensionId = this.getNextFreeDimensionId(); // Create the world properties return this.createWorld(worldArchetype, folderName, dimensionId); } /** * Creates the world properties for the specified settings and the dimension id. * * @param worldArchetype The world archetype * @param folderName The folder name * @param dimensionId The dimension id * @return The new or existing world properties, if creation was successful */ LanternWorldProperties createWorld(WorldArchetype worldArchetype, String folderName, int dimensionId) throws IOException { final LanternWorldArchetype settings0 = (LanternWorldArchetype) checkNotNull(worldArchetype, "worldArchetype"); final String worldName = worldArchetype.getName(); WorldLookupEntry entry = this.worldByName.get(worldName); if (entry != null) { return entry.properties; } WorldConfig worldConfig; // Create a config try { worldConfig = this.getOrCreateWorldConfig(worldName).config; } catch (IOException e) { throw new IOException("Unable to read/write the world config, please fix this issue before" + " creating the world.", e); } // Create the world properties final LanternWorldProperties worldProperties = new LanternWorldProperties(settings0.getName(), worldConfig); worldProperties.update(settings0); // Get the world folder final Path worldFolder = this.getWorldFolder(folderName, dimensionId); try { Files.createDirectories(worldFolder); } catch (IOException e) { throw new IOException("Unable to create the world folders for " + settings0.getName(), e); } // Store the new properties this.addWorldProperties(worldProperties, worldFolder, dimensionId); Sponge.getEventManager().post(SpongeEventFactory.createConstructWorldPropertiesEvent( Cause.source(Lantern.getMinecraftPlugin()).build(), worldArchetype, worldProperties)); // Save the world properties to reserve the world folder this.saveWorldProperties(worldProperties); return worldProperties; } /** * Loads a {@link World} from the default storage container. If a world with * the given UUID is already loaded then it is returned instead. * * @param uniqueId the uuid to lookup * @return the world, if found */ public Optional<World> loadWorld(UUID uniqueId) { checkNotNull(uniqueId, "uniqueId"); return this.loadWorld(this.worldByUUID.get(uniqueId)); } /** * Loads a {@link World} from the default storage container. If the world * associated with the given properties is already loaded then it is * returned instead. * * @param worldProperties the properties of the world to load * @return the world, if found */ public Optional<World> loadWorld(WorldProperties worldProperties) { checkNotNull(worldProperties, "worldProperties"); return this.loadWorld(this.worldByProperties.get(worldProperties)); } /** * Loads a {@link World} from the default storage container. If a world with * the given name is already loaded then it is returned instead. * * @param worldName the name to lookup * @return the world, if found */ public Optional<World> loadWorld(String worldName) { checkNotNull(worldName, "worldName"); return this.loadWorld(this.worldByName.get(worldName)); } /** * Loads a {@link World} for the world entry if possible. * * @param worldEntry the world entry * @return the world, if found */ Optional<World> loadWorld(@Nullable WorldLookupEntry worldEntry) { if (worldEntry == null) { return Optional.empty(); } if (worldEntry.world != null) { return Optional.of(worldEntry.world); } WorldConfigResult result; try { result = this.getOrCreateWorldConfig(worldEntry.properties.getWorldName()); } catch (IOException e) { this.game.getLogger().error("Unable to read the world config, please fix this issue before loading the world.", e); return Optional.empty(); } Scoreboard scoreboard; try { scoreboard = ScoreboardIO.read(worldEntry.folder); } catch (IOException e) { this.game.getLogger().error("Unable to read the scoreboard data.", e); scoreboard = Scoreboard.builder().build(); } // Create the world instance final LanternWorld world = new LanternWorld(this.game, result.config, worldEntry.folder, scoreboard, worldEntry.properties); // Share the world instance worldEntry.world = world; worldEntry.properties.setWorld(world); // Initialize the world if not done before world.initialize(); // Generate the spawn if needed if (worldEntry.properties.doesKeepSpawnLoaded()) { world.enableSpawnArea(true); } // Load the chunk loading tickets, they may load some chunks try { world.getChunkManager().loadTickets(); } catch (IOException e) { this.game.getLogger().warn("An error occurred while loading the chunk loading tickets", e); } final LoadWorldEvent event = SpongeEventFactory.createLoadWorldEvent(Cause.source(Lantern.getMinecraftPlugin()).build(), world); Sponge.getEventManager().post(event); if (event.isCancelled()) { return Optional.empty(); } // The world is ready for ticks this.addWorldTask(world); return Optional.of(world); } private static class WorldConfigResult { public final WorldConfig config; public final boolean newCreated; public WorldConfigResult(WorldConfig config, boolean newCreated) { this.newCreated = newCreated; this.config = config; } } /** * Gets or creates a new world config for the specified world. * * @param worldName The name of the world * @return The world config * @throws IOException */ private WorldConfigResult getOrCreateWorldConfig(String worldName) throws IOException { checkNotNull(worldName, "worldName"); final Path path = this.globalConfig.getPath().getParent().resolve("worlds") .resolve(worldName).resolve(WORLD_CONFIG); boolean newCreated = !Files.exists(path); final WorldConfig config = new WorldConfig(this.globalConfig, path); config.load(); return new WorldConfigResult(config, newCreated); } /** * Adds the task for the world to tick it. */ private void addWorldTask(LanternWorld world) { if (this.worldThreads.containsKey(world)) { return; } final Thread thread = ThreadHelper.newFastThreadLocalThread(thread0 -> { try { while (!thread0.isInterrupted() && !this.tickEnd.isTerminated()) { this.tickBegin.arriveAndAwaitAdvance(); try { world.pulse(); } catch (Exception e) { this.game.getLogger().error("Error occurred while pulsing the world {}", world.getName(), e); } finally { this.tickEnd.arriveAndAwaitAdvance(); } } } finally { this.tickBegin.arriveAndDeregister(); this.tickEnd.arriveAndDeregister(); } }, "world-" + world.getName()); this.worldThreads.put(world, thread); this.tickBegin.register(); this.tickEnd.register(); thread.start(); } /** * Removes the task for the world to tick it. */ private void removeWorldTask(LanternWorld world) { if (!this.worldThreads.containsKey(world)) { return; } this.worldThreads.remove(world).interrupt(); } // The current tick that is executing private volatile int currentTick = -1; private void tickEnd() { int nextTick = this.currentTick + 1; // Mark ourselves as arrived so world threads automatically trigger advance once done int endPhase = this.tickEnd.arriveAndAwaitAdvance(); if (endPhase != nextTick) { this.game.getLogger().warn("Tick end barrier {} has advanced differently from tick begin barrier: {}", endPhase, nextTick); } } /** * Pulses the world for the next tick. */ public void pulse() { try { this.tickEnd.awaitAdvanceInterruptibly(this.currentTick); this.currentTick = this.tickBegin.arrive(); try { this.executor.submit(this::tickEnd); } catch (RejectedExecutionException ex) { this.shutdown(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /** * Shutdown the all the world threads, the executor and * unloads all the active worlds. */ public void shutdown() { // Unload all the active worlds this.worldByProperties.values().stream().filter(entry -> entry.world != null).forEach(entry -> this.unloadWorld(entry.world)); this.tickBegin.forceTermination(); this.tickEnd.forceTermination(); this.worldThreads.clear(); this.executor.shutdown(); } /** * Gets the world folder for the dimension id. * * @param dimensionId the dimension id * @return the world folder */ private Path getWorldFolder(String folderName, int dimensionId) { return dimensionId == 0 ? this.rootWorldDirectory : this.rootWorldDirectory.resolve(folderName); } private static final int MIN_CUSTOM_DIMENSION_ID = 2; /** * Gets the next available dimension id. * * @return the next dimension id */ private int getNextFreeDimensionId() { // Keep the ids -1; 0; 1 safe int next = MIN_CUSTOM_DIMENSION_ID; while (true) { next = this.dimensionMap.nextClearBit(next); if (this.worldByDimensionId.containsKey(next)) { this.dimensionMap.set(next); } else { return next; } } } private void addUpdatedWorldProperties(LanternWorldProperties properties, Path worldFolder, @Nullable Integer dimensionId) { // The world is already added if (this.worldByUUID.containsKey(properties.getUniqueId())) { return; } // Get the dimension id and make sure that it's not already used if (dimensionId == null || this.worldByDimensionId.containsValue(dimensionId)) { dimensionId = this.getNextFreeDimensionId(); // Ignore the root dimension } else if (dimensionId > MIN_CUSTOM_DIMENSION_ID) { this.dimensionMap.set(dimensionId); } this.addWorldProperties(properties, worldFolder, dimensionId); } /** * Adds the world properties. * * @param properties The properties * @param worldDirectory The directory of the world * @param dimensionId The id of the world (dimension) * @return The world lookup entry */ private WorldLookupEntry addWorldProperties(LanternWorldProperties properties, Path worldDirectory, int dimensionId) { final WorldLookupEntry entry = new WorldLookupEntry(properties, worldDirectory, dimensionId); this.worldByUUID.put(properties.getUniqueId(), entry); this.worldByName.put(properties.getWorldName(), entry); this.worldByDimensionId.put(dimensionId, entry); this.worldByProperties.put(properties, entry); return entry; } /** * All the directories that should be ignored while loading worlds. We will * also add the nether and the end manually. */ private final Set<String> ignoredDirectoryNames = Sets.newHashSet("data", "playerdata", "region", "stats", "dim1", "dim-1"); /** * Initializes the root world and the dimension id map. */ public void init() throws IOException { // The properties of the root world LanternWorldProperties rootWorldProperties = null; LevelData levelData; if (Files.exists(this.rootWorldDirectory)) { try { levelData = LanternWorldPropertiesIO.read(this.rootWorldDirectory, null, null); // Create a config try { WorldConfigResult result = this.getOrCreateWorldConfig(levelData.worldName); rootWorldProperties = LanternWorldPropertiesIO.convert(levelData, result.config, result.newCreated); if (result.newCreated) { result.config.save(); } } catch (IOException e) { this.game.getLogger().error("Unable to read/write the root world config, please fix this issue before loading the world.", e); throw e; } // Already store the data this.addUpdatedWorldProperties(rootWorldProperties, this.rootWorldDirectory, 0); } catch (FileNotFoundException e) { // We can ignore this exception, because this means // that we have to generate the world } catch (IOException e) { this.game.getLogger().error("Unable to load root world, please fix this issue before starting the server.", e); throw e; } } // Always use a new dimension map, we will scan for the worlds // through folders and the dimension ids will be generated or // refreshed if needed this.dimensionMap = new BitSet(); LanternWorldProperties rootWorldProperties0 = rootWorldProperties; // Generate the root (default) world if missing if (rootWorldProperties0 == null) { final String name = "Overworld"; // TODO: Use the default generator type once implemented rootWorldProperties0 = this.createWorld(WorldArchetype.builder() .from(WorldArchetypes.OVERWORLD) //.generator(GeneratorTypes.DEBUG) .generator(GeneratorTypes.FLAT) .build(name, name), "", 0); } // Get all the dimensions (worlds) that should be loaded final List<WorldLookupEntry> loadQueue = new ArrayList<>(1); // Add the root dimension loadQueue.add(this.worldByDimensionId.get(0)); // TODO: The end and nether final Map<Integer, Tuple<Path, LevelData>> idToLevelData = new HashMap<>(); final List<Tuple<Path, LevelData>> levelDataWithoutId = new ArrayList<>(); if (rootWorldProperties != null) { for (Path path : Files.list(this.rootWorldDirectory).filter(Files::isDirectory).collect(Collectors.toList())) { if (Files.list(path).count() == 0 || this.ignoredDirectoryNames.contains(path.getFileName().toString().toLowerCase())) { continue; } try { try { levelData = LanternWorldPropertiesIO.read(path, null, null); } catch (FileNotFoundException e) { Lantern.getLogger().info("Found a invalid world directory {} inside the root world directory, the level.dat file is missing", path.getFileName().toString()); continue; } Integer dimensionId = levelData.dimensionId; Tuple<Path, LevelData> tuple = Tuple.of(path, levelData); if (dimensionId == null || idToLevelData.containsValue(dimensionId) || dimensionId < MIN_CUSTOM_DIMENSION_ID) { levelDataWithoutId.add(tuple); } else { idToLevelData.put(dimensionId, tuple); } } catch (Exception e) { Lantern.getLogger().info("Unable to load the world in the directory {}", path.getFileName().toString()); } } } // Generate a dimension id for all the worlds that need it for (Tuple<Path, LevelData> tuple : levelDataWithoutId) { idToLevelData.put(this.getNextFreeDimensionId(), tuple); } levelDataWithoutId.clear(); // Load the world properties and config files for all the worlds for (Map.Entry<Integer, Tuple<Path, LevelData>> entry : idToLevelData.entrySet()) { levelData = entry.getValue().getSecond(); LanternWorldProperties worldProperties; try { WorldConfigResult result = this.getOrCreateWorldConfig(levelData.worldName); worldProperties = LanternWorldPropertiesIO.convert(levelData, result.config, result.newCreated); if (result.newCreated) { result.config.save(); } } catch (IOException e) { this.game.getLogger().error("Unable to read/write the world config, please fix this issue before loading the world.", e); throw e; } // Store the world properties WorldLookupEntry lookupEntry = this.addWorldProperties(worldProperties, entry.getValue().getFirst(), entry.getKey()); // Check if it should be loaded on startup if (worldProperties.loadOnStartup()) { loadQueue.add(lookupEntry); } } idToLevelData.clear(); // The root world must be enabled rootWorldProperties0.setEnabled(true); // Load all the worlds loadQueue.forEach(this::loadWorld); } }