package com.forgeessentials.multiworld; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldManager; import net.minecraft.world.WorldProvider; import net.minecraft.world.WorldServer; import net.minecraft.world.WorldSettings; import net.minecraft.world.WorldSettings.GameType; import net.minecraft.world.WorldType; import net.minecraft.world.storage.ISaveHandler; import net.minecraftforge.common.DimensionManager; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.network.ForgeMessage.DimensionRegisterMessage; import net.minecraftforge.event.world.WorldEvent; import org.apache.commons.io.FileUtils; import com.forgeessentials.api.APIRegistry; import com.forgeessentials.api.NamedWorldHandler; import com.forgeessentials.api.permissions.FEPermissions; import com.forgeessentials.api.permissions.WorldZone; import com.forgeessentials.api.permissions.Zone; import com.forgeessentials.data.v2.DataManager; import com.forgeessentials.multiworld.MultiworldException.Type; import com.forgeessentials.multiworld.gen.WorldTypeMultiworld; import com.forgeessentials.util.events.ServerEventHandler; import com.forgeessentials.util.output.LoggingHandler; import com.google.common.collect.ImmutableMap; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent; import cpw.mods.fml.common.network.FMLEmbeddedChannel; import cpw.mods.fml.common.network.FMLOutboundHandler; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.relauncher.Side; /** * * @author Olee * @author gnif */ public class MultiworldManager extends ServerEventHandler implements NamedWorldHandler { public static final String PERM_PROP_MULTIWORLD = FEPermissions.FE_INTERNAL + ".multiworld"; public static final String PROVIDER_NORMAL = "normal"; public static final String PROVIDER_HELL = "nether"; public static final String PROVIDER_END = "end"; public static final WorldTypeMultiworld WORLD_TYPE_MULTIWORLD = new WorldTypeMultiworld(); // ============================================================ /** * Registered multiworlds */ protected Map<String, Multiworld> worlds = new HashMap<String, Multiworld>(); /** * Registered multiworlds by dimension */ protected Map<Integer, Multiworld> worldsByDim = new HashMap<Integer, Multiworld>(); /** * Mapping from provider classnames to IDs */ protected Map<String, Integer> worldProviderClasses = new HashMap<String, Integer>(); /** * Mapping from worldType names to WorldType objects */ protected Map<String, WorldType> worldTypes = new HashMap<String, WorldType>(); /** * List of worlds that have been marked for deletion */ protected ArrayList<WorldServer> worldsToDelete = new ArrayList<WorldServer>(); /** * List of worlds that have been marked for removal */ protected ArrayList<WorldServer> worldsToRemove = new ArrayList<WorldServer>(); /** * Event handler for new clients that need to know about our worlds */ protected MultiworldEventHandler eventHandler = new MultiworldEventHandler(this); private NamedWorldHandler parentNamedWorldHandler; // ============================================================ public MultiworldManager() { parentNamedWorldHandler = APIRegistry.namedWorldHandler; APIRegistry.namedWorldHandler = this; } public void saveAll() { for (Multiworld world : getWorlds()) { world.save(); } } public void load() { DimensionManager.loadDimensionDataMap(null); Map<String, Multiworld> loadedWorlds = DataManager.getInstance().loadAll(Multiworld.class); for (Multiworld world : loadedWorlds.values()) { worlds.put(world.getName(), world); try { loadWorld(world); } catch (MultiworldException e) { switch (e.type) { case NO_PROVIDER: LoggingHandler.felog.error(String.format(e.type.error, world.provider)); break; case NO_WORLDTYPE: LoggingHandler.felog.error(String.format(e.type.error, world.worldType)); break; default: LoggingHandler.felog.error(e.type.error); break; } } } } public Collection<Multiworld> getWorlds() { return worlds.values(); } public ImmutableMap<String, Multiworld> getWorldMap() { return ImmutableMap.copyOf(worlds); } public Set<Integer> getDimensions() { return worldsByDim.keySet(); } public Multiworld getMultiworld(int dimensionId) { return worldsByDim.get(dimensionId); } public Multiworld getMultiworld(String name) { return worlds.get(name); } @Override public WorldServer getWorld(String name) { WorldServer world = parentNamedWorldHandler.getWorld(name); if (world != null) return world; Multiworld mw = getMultiworld(name); if (mw != null) return mw.getWorldServer(); return null; } @Override public String getWorldName(int dimId) { Multiworld mw = getMultiworld(dimId); if (mw != null) return mw.getName(); return parentNamedWorldHandler.getWorldName(dimId); } /** * Register and load a multiworld. If the world fails to load, it won't be registered */ public void addWorld(Multiworld world) throws MultiworldException { if (worlds.containsKey(world.getName())) throw new MultiworldException(Type.ALREADY_EXISTS); loadWorld(world); worlds.put(world.getName(), world); world.save(); } /** * Get a free dimensionID for a new multiworld - minimum dim-id is 10 */ public static int getFreeDimensionId() { int id = 10; while (DimensionManager.isDimensionRegistered(id)) id++; return id; } /** * Loads a multiworld */ protected void loadWorld(Multiworld world) throws MultiworldException { if (world.worldLoaded) return; try { world.providerId = getWorldProviderId(world.provider); world.worldTypeObj = getWorldTypeByName(world.worldType); // Register dimension with last used id if possible if (DimensionManager.isDimensionRegistered(world.dimensionId)) world.dimensionId = getFreeDimensionId(); // Handle permission-dim changes checkMultiworldPermissions(world); APIRegistry.perms.getServerZone().getWorldZone(world.dimensionId) .setGroupPermissionProperty(Zone.GROUP_DEFAULT, PERM_PROP_MULTIWORLD, world.getName()); // Register the dimension DimensionManager.registerDimension(world.dimensionId, world.providerId); worldsByDim.put(world.dimensionId, world); // Initialize world settings MinecraftServer mcServer = MinecraftServer.getServer(); WorldServer overworld = DimensionManager.getWorld(0); if (overworld == null) throw new RuntimeException("Cannot hotload dim: Overworld is not Loaded!"); ISaveHandler savehandler = new MultiworldSaveHandler(overworld.getSaveHandler(), world); WorldSettings worldSettings = new WorldSettings(world.seed, GameType.SURVIVAL, world.mapFeaturesEnabled, false, world.worldTypeObj); // Create WorldServer with settings WorldServer worldServer = new WorldServerMultiworld(mcServer, savehandler, // overworld.getWorldInfo().getWorldName(), world.dimensionId, worldSettings, // overworld, mcServer.theProfiler, world); // Overwrite dimensionId because WorldProviderEnd for example just hardcodes the dimId worldServer.provider.dimensionId = world.dimensionId; worldServer.addWorldAccess(new WorldManager(mcServer, worldServer)); if (!mcServer.isSinglePlayer()) worldServer.getWorldInfo().setGameType(mcServer.getGameType()); mcServer.func_147139_a(mcServer.func_147135_j()); world.updateWorldSettings(); world.worldLoaded = true; world.error = false; // Post WorldEvent.Load MinecraftForge.EVENT_BUS.post(new WorldEvent.Load(worldServer)); // Tell everyone about the new dim FMLEmbeddedChannel channel = NetworkRegistry.INSTANCE.getChannel("FORGE", Side.SERVER); DimensionRegisterMessage msg = new DimensionRegisterMessage(world.dimensionId, world.providerId); channel.attr(FMLOutboundHandler.FML_MESSAGETARGET).set(FMLOutboundHandler.OutboundTarget.ALL); channel.writeOutbound(msg); } catch (Exception e) { world.error = true; throw e; } } public int getWorldProviderId(String providerName) throws MultiworldException { switch (providerName.toLowerCase()) { // We use the hardcoded values as some mods just replace the class // (BiomesOPlenty) case PROVIDER_NORMAL: return 0; case PROVIDER_HELL: return -1; case PROVIDER_END: return 1; default: // Otherwise we try to use the provider classname that was supplied Integer providerId = worldProviderClasses.get(providerName); if (providerId == null) throw new MultiworldException(Type.NO_PROVIDER); return providerId; } } /** * Checks the WorldZone permissions for multiworlds and moves them to the correct dimension if it changed */ private static void checkMultiworldPermissions(Multiworld world) { for (WorldZone zone : APIRegistry.perms.getServerZone().getWorldZones().values()) { String wn = zone.getGroupPermission(Zone.GROUP_DEFAULT, PERM_PROP_MULTIWORLD); if (wn != null && wn.equals(world.getName())) { if (zone.getDimensionID() != world.dimensionId) { WorldZone newZone = APIRegistry.perms.getServerZone().getWorldZone(world.dimensionId); // Swap the permissions of the multiworld with the one // that's currently taking up it's dimID zone.swapPermissions(newZone); } return; } } } // ============================================================ /** * Unload world * * @param world */ public void unloadWorld(Multiworld world) { world.worldLoaded = false; world.removeAllPlayersFromWorld(); DimensionManager.unloadWorld(world.getDimensionId()); worldsToRemove.add(DimensionManager.getWorld(world.getDimensionId())); worldsByDim.remove(world.getDimensionId()); worlds.remove(world.getName()); } /** * Unload world and delete it's data once onloaded * * @param world */ public void deleteWorld(Multiworld world) { unloadWorld(world); world.delete(); worldsToDelete.add(DimensionManager.getWorld(world.getDimensionId())); } /** * Remove dimensions and clear multiworld-data when server stopped * * (for integrated server) */ public void serverStopped() { saveAll(); for (Multiworld world : worlds.values()) { world.worldLoaded = false; DimensionManager.unregisterDimension(world.getDimensionId()); } worldsByDim.clear(); worlds.clear(); } // ============================================================ /** * Forge DimensionManager stores used dimension IDs and does not assign them again, unless they are cleared * manually. */ public void clearDimensionMap() { DimensionManager.loadDimensionDataMap(null); } // ============================================================ // Unloading and deleting of worlds /** * When a world is unloaded and marked as to-be-unregistered, remove it now when it is not needed any more */ @SubscribeEvent public void serverTickEvent(ServerTickEvent event) { unregisterDimensions(); deleteDimensions(); } /** * Load global world data */ @SubscribeEvent public void worldUnloadEvent(WorldEvent.Unload event) { unregisterDimensions(); deleteDimensions(); } /** * Unregister all worlds that have been marked for removal */ protected void unregisterDimensions() { for (Iterator<WorldServer> it = worldsToRemove.iterator(); it.hasNext();) { WorldServer world = it.next(); // Check with DimensionManager, whether the world is still loaded if (DimensionManager.getWorld(world.provider.dimensionId) == null) { if (DimensionManager.isDimensionRegistered(world.provider.dimensionId)) DimensionManager.unregisterDimension(world.provider.dimensionId); it.remove(); } } } /** * Delete all worlds that have been marked for deletion */ protected void deleteDimensions() { for (Iterator<WorldServer> it = worldsToDelete.iterator(); it.hasNext();) { WorldServer world = it.next(); // Check with DimensionManager, whether the world is still loaded if (DimensionManager.getWorld(world.provider.dimensionId) == null) { try { if (DimensionManager.isDimensionRegistered(world.provider.dimensionId)) DimensionManager.unregisterDimension(world.provider.dimensionId); File path = world.getChunkSaveLocation(); // new // File(world.getSaveHandler().getWorldDirectory(), // world.provider.getSaveFolder()); FileUtils.deleteDirectory(path); it.remove(); } catch (IOException e) { LoggingHandler.felog.warn("Error deleting dimension files"); } } } } // ============================================================ // WorldProvider management /** * Use reflection to load the registered WorldProviders */ public void loadWorldProviders() { try { Field f_providers = DimensionManager.class.getDeclaredField("providers"); f_providers.setAccessible(true); @SuppressWarnings("unchecked") Hashtable<Integer, Class<? extends WorldProvider>> loadedProviders = (Hashtable<Integer, Class<? extends WorldProvider>>) f_providers.get(null); for (Entry<Integer, Class<? extends WorldProvider>> provider : loadedProviders.entrySet()) { // skip the default providers as these are aliased as 'normal', // 'nether' and 'end' if (provider.getKey() >= -1 && provider.getKey() <= 1) continue; worldProviderClasses.put(provider.getValue().getName(), provider.getKey()); } worldProviderClasses.put(PROVIDER_NORMAL, 0); worldProviderClasses.put(PROVIDER_HELL, 1); worldProviderClasses.put(PROVIDER_END, -1); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } LoggingHandler.felog.debug("[Multiworld] Available world providers:"); for (Entry<String, Integer> provider : worldProviderClasses.entrySet()) { LoggingHandler.felog.debug("# " + provider.getValue() + ":" + provider.getKey()); } } public Map<String, Integer> getWorldProviders() { return worldProviderClasses; } // ============================================================ // WorldType management /** * Returns the WorldType for a given worldType string */ public WorldType getWorldTypeByName(String worldType) throws MultiworldException { WorldType type = worldTypes.get(worldType.toUpperCase()); if (type == null) throw new MultiworldException(Type.NO_WORLDTYPE); return type; } /** * Builds the map of valid worldTypes */ public void loadWorldTypes() { for (int i = 0; i < WorldType.worldTypes.length; ++i) { WorldType type = WorldType.worldTypes[i]; if (type == null) continue; String name = type.getWorldTypeName().toUpperCase(); /* * MC does not allow creation of this worldType, so we should not either */ if (name.equals("DEFAULT_1_1")) continue; worldTypes.put(name, type); } LoggingHandler.felog.debug("[Multiworld] Available world types:"); for (String worldType : worldTypes.keySet()) LoggingHandler.felog.debug("# " + worldType); } public Map<String, WorldType> getWorldTypes() { return worldTypes; } }