001package net.minecraftforge.common; 002 003import java.io.File; 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.BitSet; 007import java.util.HashSet; 008import java.util.Hashtable; 009import java.util.List; 010import java.util.ListIterator; 011import java.util.Map; 012import java.util.Map.Entry; 013import java.util.Set; 014import java.util.concurrent.ConcurrentMap; 015import java.util.logging.Level; 016 017import com.google.common.collect.ArrayListMultimap; 018import com.google.common.collect.ImmutableListMultimap; 019import com.google.common.collect.ListMultimap; 020import com.google.common.collect.Lists; 021import com.google.common.collect.MapMaker; 022import com.google.common.collect.Maps; 023import com.google.common.collect.Sets; 024 025import cpw.mods.fml.common.FMLCommonHandler; 026import cpw.mods.fml.common.FMLLog; 027 028import net.minecraft.nbt.NBTTagCompound; 029import net.minecraft.server.MinecraftServer; 030import net.minecraft.world.ChunkCoordIntPair; 031import net.minecraft.world.MinecraftException; 032import net.minecraft.world.World; 033import net.minecraft.world.WorldManager; 034import net.minecraft.world.WorldProvider; 035import net.minecraft.world.WorldProviderEnd; 036import net.minecraft.world.WorldProviderHell; 037import net.minecraft.world.WorldProviderSurface; 038import net.minecraft.world.WorldServer; 039import net.minecraft.world.WorldServerMulti; 040import net.minecraft.world.WorldSettings; 041import net.minecraft.world.storage.ISaveHandler; 042import net.minecraft.world.storage.SaveHandler; 043import net.minecraftforge.event.world.WorldEvent; 044 045public class DimensionManager 046{ 047 private static Hashtable<Integer, Class<? extends WorldProvider>> providers = new Hashtable<Integer, Class<? extends WorldProvider>>(); 048 private static Hashtable<Integer, Boolean> spawnSettings = new Hashtable<Integer, Boolean>(); 049 private static Hashtable<Integer, WorldServer> worlds = new Hashtable<Integer, WorldServer>(); 050 private static boolean hasInit = false; 051 private static Hashtable<Integer, Integer> dimensions = new Hashtable<Integer, Integer>(); 052 private static ArrayList<Integer> unloadQueue = new ArrayList<Integer>(); 053 private static BitSet dimensionMap = new BitSet(Long.SIZE << 4); 054 private static ConcurrentMap<World, World> weakWorldMap = new MapMaker().weakKeys().weakValues().<World,World>makeMap(); 055 private static Set<Integer> leakedWorlds = Sets.newHashSet(); 056 057 public static boolean registerProviderType(int id, Class<? extends WorldProvider> provider, boolean keepLoaded) 058 { 059 if (providers.containsKey(id)) 060 { 061 return false; 062 } 063 providers.put(id, provider); 064 spawnSettings.put(id, keepLoaded); 065 return true; 066 } 067 068 /** 069 * Unregisters a Provider type, and returns a array of all dimensions that are 070 * registered to this provider type. 071 * If the return size is greater then 0, it is required that the caller either 072 * change those dimensions's registered type, or replace this type before the 073 * world is attempted to load, else the loader will throw an exception. 074 * 075 * @param id The provider type ID to unreigster 076 * @return An array containing all dimension IDs still registered to this provider type. 077 */ 078 public static int[] unregisterProviderType(int id) 079 { 080 if (!providers.containsKey(id)) 081 { 082 return new int[0]; 083 } 084 providers.remove(id); 085 spawnSettings.remove(id); 086 087 int[] ret = new int[dimensions.size()]; 088 int x = 0; 089 for (Map.Entry<Integer, Integer> ent : dimensions.entrySet()) 090 { 091 if (ent.getValue() == id) 092 { 093 ret[x++] = ent.getKey(); 094 } 095 } 096 097 return Arrays.copyOf(ret, x); 098 } 099 100 public static void init() 101 { 102 if (hasInit) 103 { 104 return; 105 } 106 107 hasInit = true; 108 109 registerProviderType( 0, WorldProviderSurface.class, true); 110 registerProviderType(-1, WorldProviderHell.class, true); 111 registerProviderType( 1, WorldProviderEnd.class, false); 112 registerDimension( 0, 0); 113 registerDimension(-1, -1); 114 registerDimension( 1, 1); 115 } 116 117 public static void registerDimension(int id, int providerType) 118 { 119 if (!providers.containsKey(providerType)) 120 { 121 throw new IllegalArgumentException(String.format("Failed to register dimension for id %d, provider type %d does not exist", id, providerType)); 122 } 123 if (dimensions.containsKey(id)) 124 { 125 throw new IllegalArgumentException(String.format("Failed to register dimension for id %d, One is already registered", id)); 126 } 127 dimensions.put(id, providerType); 128 if (id >= 0) 129 { 130 dimensionMap.set(id); 131 } 132 } 133 134 /** 135 * For unregistering a dimension when the save is changed (disconnected from a server or loaded a new save 136 */ 137 public static void unregisterDimension(int id) 138 { 139 if (!dimensions.containsKey(id)) 140 { 141 throw new IllegalArgumentException(String.format("Failed to unregister dimension for id %d; No provider registered", id)); 142 } 143 dimensions.remove(id); 144 } 145 146 public static int getProviderType(int dim) 147 { 148 if (!dimensions.containsKey(dim)) 149 { 150 throw new IllegalArgumentException(String.format("Could not get provider type for dimension %d, does not exist", dim)); 151 } 152 return dimensions.get(dim); 153 } 154 155 public static WorldProvider getProvider(int dim) 156 { 157 return getWorld(dim).provider; 158 } 159 160 public static Integer[] getIDs(boolean check) 161 { 162 if (check) 163 { 164 List<World> allWorlds = Lists.newArrayList(weakWorldMap.keySet()); 165 allWorlds.removeAll(worlds.values()); 166 Set<Integer> newLeaks = Sets.newHashSet(); 167 for (ListIterator<World> li = allWorlds.listIterator(); li.hasNext(); ) 168 { 169 World w = li.next(); 170 if (leakedWorlds.contains(System.identityHashCode(w))) 171 { 172 li.remove(); 173 } 174 newLeaks.add(System.identityHashCode(w)); 175 } 176 leakedWorlds = newLeaks; 177 if (allWorlds.size() > 0) 178 { 179 FMLLog.severe("Detected leaking worlds in memory. There are %d worlds that appear to be persisting. A mod is likely caching the world incorrectly\n", allWorlds.size() + leakedWorlds.size()); 180 for (World w : allWorlds) 181 { 182 FMLLog.severe("The world %x (%s) has leaked.\n", System.identityHashCode(w), w.getWorldInfo().getWorldName()); 183 } 184 } 185 } 186 return getIDs(); 187 } 188 public static Integer[] getIDs() 189 { 190 return worlds.keySet().toArray(new Integer[worlds.size()]); //Only loaded dims, since usually used to cycle through loaded worlds 191 } 192 193 public static void setWorld(int id, WorldServer world) 194 { 195 if (world != null) { 196 worlds.put(id, world); 197 weakWorldMap.put(world, world); 198 MinecraftServer.getServer().worldTickTimes.put(id, new long[100]); 199 FMLLog.info("Loading dimension %d (%s) (%s)", id, world.getWorldInfo().getWorldName(), world.getMinecraftServer()); 200 } else { 201 worlds.remove(id); 202 MinecraftServer.getServer().worldTickTimes.remove(id); 203 FMLLog.info("Unloading dimension %d", id); 204 } 205 206 ArrayList<WorldServer> tmp = new ArrayList<WorldServer>(); 207 if (worlds.get( 0) != null) 208 tmp.add(worlds.get( 0)); 209 if (worlds.get(-1) != null) 210 tmp.add(worlds.get(-1)); 211 if (worlds.get( 1) != null) 212 tmp.add(worlds.get( 1)); 213 214 for (Entry<Integer, WorldServer> entry : worlds.entrySet()) 215 { 216 int dim = entry.getKey(); 217 if (dim >= -1 && dim <= 1) 218 { 219 continue; 220 } 221 tmp.add(entry.getValue()); 222 } 223 224 MinecraftServer.getServer().worldServers = tmp.toArray(new WorldServer[tmp.size()]); 225 } 226 227 public static void initDimension(int dim) { 228 WorldServer overworld = getWorld(0); 229 if (overworld == null) { 230 throw new RuntimeException("Cannot Hotload Dim: Overworld is not Loaded!"); 231 } 232 try { 233 DimensionManager.getProviderType(dim); 234 } catch (Exception e) { 235 System.err.println("Cannot Hotload Dim: " + e.getMessage()); 236 return; //If a provider hasn't been registered then we can't hotload the dim 237 } 238 MinecraftServer mcServer = overworld.getMinecraftServer(); 239 ISaveHandler savehandler = overworld.getSaveHandler(); 240 WorldSettings worldSettings = new WorldSettings(overworld.getWorldInfo()); 241 242 WorldServer world = (dim == 0 ? overworld : new WorldServerMulti(mcServer, savehandler, overworld.getWorldInfo().getWorldName(), dim, worldSettings, overworld, mcServer.theProfiler)); 243 world.addWorldAccess(new WorldManager(mcServer, world)); 244 MinecraftForge.EVENT_BUS.post(new WorldEvent.Load(world)); 245 if (!mcServer.isSinglePlayer()) 246 { 247 world.getWorldInfo().setGameType(mcServer.getGameType()); 248 } 249 250 mcServer.setDifficultyForAllWorlds(mcServer.getDifficulty()); 251 } 252 253 public static WorldServer getWorld(int id) 254 { 255 return worlds.get(id); 256 } 257 258 public static WorldServer[] getWorlds() 259 { 260 return worlds.values().toArray(new WorldServer[worlds.size()]); 261 } 262 263 public static boolean shouldLoadSpawn(int dim) 264 { 265 int id = getProviderType(dim); 266 return spawnSettings.containsKey(id) && spawnSettings.get(id); 267 } 268 269 static 270 { 271 init(); 272 } 273 274 /** 275 * Not public API: used internally to get dimensions that should load at 276 * server startup 277 */ 278 public static Integer[] getStaticDimensionIDs() 279 { 280 return dimensions.keySet().toArray(new Integer[dimensions.keySet().size()]); 281 } 282 public static WorldProvider createProviderFor(int dim) 283 { 284 try 285 { 286 if (dimensions.containsKey(dim)) 287 { 288 WorldProvider provider = providers.get(getProviderType(dim)).newInstance(); 289 provider.setDimension(dim); 290 return provider; 291 } 292 else 293 { 294 throw new RuntimeException(String.format("No WorldProvider bound for dimension %d", dim)); //It's going to crash anyway at this point. Might as well be informative 295 } 296 } 297 catch (Exception e) 298 { 299 FMLCommonHandler.instance().getFMLLogger().log(Level.SEVERE,String.format("An error occured trying to create an instance of WorldProvider %d (%s)", 300 dim, providers.get(getProviderType(dim)).getSimpleName()),e); 301 throw new RuntimeException(e); 302 } 303 } 304 305 public static void unloadWorld(int id) { 306 unloadQueue.add(id); 307 } 308 309 /* 310 * To be called by the server at the appropriate time, do not call from mod code. 311 */ 312 public static void unloadWorlds(Hashtable<Integer, long[]> worldTickTimes) { 313 for (int id : unloadQueue) { 314 WorldServer w = worlds.get(id); 315 try { 316 if (w != null) 317 { 318 w.saveAllChunks(true, null); 319 } 320 else 321 { 322 FMLLog.warning("Unexpected world unload - world %d is already unloaded", id); 323 } 324 } catch (MinecraftException e) { 325 e.printStackTrace(); 326 } 327 finally 328 { 329 if (w != null) 330 { 331 MinecraftForge.EVENT_BUS.post(new WorldEvent.Unload(w)); 332 w.flush(); 333 setWorld(id, null); 334 } 335 } 336 } 337 unloadQueue.clear(); 338 } 339 340 /** 341 * Return the next free dimension ID. Note: you are not guaranteed a contiguous 342 * block of free ids. Always call for each individual ID you wish to get. 343 * @return the next free dimension ID 344 */ 345 public static int getNextFreeDimId() { 346 int next = 0; 347 while (true) 348 { 349 next = dimensionMap.nextClearBit(next); 350 if (dimensions.containsKey(next)) 351 { 352 dimensionMap.set(next); 353 } 354 else 355 { 356 return next; 357 } 358 } 359 } 360 361 public static NBTTagCompound saveDimensionDataMap() 362 { 363 int[] data = new int[(dimensionMap.length() + Integer.SIZE - 1 )/ Integer.SIZE]; 364 NBTTagCompound dimMap = new NBTTagCompound(); 365 for (int i = 0; i < data.length; i++) 366 { 367 int val = 0; 368 for (int j = 0; j < Integer.SIZE; j++) 369 { 370 val |= dimensionMap.get(i * Integer.SIZE + j) ? (1 << j) : 0; 371 } 372 data[i] = val; 373 } 374 dimMap.setIntArray("DimensionArray", data); 375 return dimMap; 376 } 377 378 public static void loadDimensionDataMap(NBTTagCompound compoundTag) 379 { 380 if (compoundTag == null) 381 { 382 dimensionMap.clear(); 383 for (Integer id : dimensions.keySet()) 384 { 385 if (id >= 0) 386 { 387 dimensionMap.set(id); 388 } 389 } 390 } 391 else 392 { 393 int[] intArray = compoundTag.getIntArray("DimensionArray"); 394 for (int i = 0; i < intArray.length; i++) 395 { 396 for (int j = 0; j < Integer.SIZE; j++) 397 { 398 dimensionMap.set(i * Integer.SIZE + j, (intArray[i] & (1 << j)) != 0); 399 } 400 } 401 } 402 } 403 404 /** 405 * Return the current root directory for the world save. Accesses getSaveHandler from the overworld 406 * @return the root directory of the save 407 */ 408 public static File getCurrentSaveRootDirectory() 409 { 410 if (DimensionManager.getWorld(0) != null) 411 { 412 return ((SaveHandler)DimensionManager.getWorld(0).getSaveHandler()).getSaveDirectory(); 413 } 414 else if (MinecraftServer.getServer() != null) 415 { 416 MinecraftServer srv = MinecraftServer.getServer(); 417 SaveHandler saveHandler = (SaveHandler) srv.getActiveAnvilConverter().getSaveLoader(srv.getFolderName(), false); 418 return saveHandler.getSaveDirectory(); 419 } 420 else 421 { 422 return null; 423 } 424 } 425}