diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 45fb7ac..ed9606e 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -712,7 +712,7 @@ public void startServerThread() { StartupQuery.reset(); - (new Thread("Server thread") + (serverThread = new Thread("Server thread") { private static final String __OBFID = "CL_00001418"; public void run() @@ -1505,10 +1505,16 @@ public long currentWait = TICK_TIME; public long pickWait = TICK_TIME; public final long startTime = System.currentTimeMillis(); + private Thread serverThread; private final MultiWorld multiworld = new MultiWorld(this); private IPermissionManager permissionManager; private final Scheduler scheduler = new Scheduler(); + public Thread getServerThread() + { + return serverThread; + } + public MultiWorld getMultiWorld() { return multiworld; @@ -1539,7 +1545,7 @@ return anvilFile; } - protected File getHomeDirectory() + public File getHomeDirectory() { return getDataDirectory(); } diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index 3968872..65c281c 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -612,7 +612,7 @@ } @Override - protected File getHomeDirectory() + public File getHomeDirectory() { return FMLLaunchHandler.getMinecraftHome(); } diff --git a/src/main/java/net/minecraft/world/chunk/storage/AnvilChunkLoader.java b/src/main/java/net/minecraft/world/chunk/storage/AnvilChunkLoader.java index f57d398..e932d29 100644 --- a/src/main/java/net/minecraft/world/chunk/storage/AnvilChunkLoader.java +++ b/src/main/java/net/minecraft/world/chunk/storage/AnvilChunkLoader.java @@ -38,9 +38,9 @@ public class AnvilChunkLoader implements IChunkLoader, IThreadedFileIO { private static final Logger logger = LogManager.getLogger(); - private final TIntObjectHashMap pendingSaves = new TIntObjectHashMap(); - private Object syncLockObject = new Object(); - public final File chunkSaveLocation; + protected final TIntObjectHashMap pendingSaves = new TIntObjectHashMap(); + protected Object syncLockObject = new Object(); + public File chunkSaveLocation; private static final String __OBFID = "CL_00000384"; public AnvilChunkLoader(File par1File) @@ -56,7 +56,12 @@ return true; } - return RegionFileCache.createOrLoadRegionFile(this.chunkSaveLocation, i, j).chunkExists(i & 31, j & 31); + return isChunkExistsInFile(i, j); + } + + protected boolean isChunkExistsInFile(int cx, int cz) + { + return RegionFileCache.createOrLoadRegionFile(this.chunkSaveLocation, cx, cz).chunkExists(cx & 31, cz & 31); } public Chunk loadChunk(World par1World, int par2, int par3) throws IOException @@ -92,7 +97,7 @@ if (nbttagcompound == null) { - DataInputStream datainputstream = RegionFileCache.getChunkInputStream(this.chunkSaveLocation, par2, par3); + DataInputStream datainputstream = getChunkInputStream(par2, par3); if (datainputstream == null) { @@ -120,6 +125,11 @@ return data; } + + protected DataInputStream getChunkInputStream(int cz, int cx) + { + return RegionFileCache.getChunkInputStream(this.chunkSaveLocation, cz, cx); + } protected Chunk checkedReadChunkFromNBT(World par1World, int par2, int par3, NBTTagCompound par4NBTTagCompound) { @@ -247,12 +257,17 @@ return true; } - private void writeChunkNBTTags(AnvilChunkLoader.PendingChunk par1AnvilChunkLoaderPending) throws IOException + protected void writeChunkNBTTags(AnvilChunkLoader.PendingChunk par1AnvilChunkLoaderPending) throws IOException { - DataOutputStream dataoutputstream = RegionFileCache.getChunkOutputStream(this.chunkSaveLocation, par1AnvilChunkLoaderPending.chunkCoordinate.chunkXPos, par1AnvilChunkLoaderPending.chunkCoordinate.chunkZPos); + DataOutputStream dataoutputstream = getChunkOutputStream(par1AnvilChunkLoaderPending); CompressedStreamTools.write(par1AnvilChunkLoaderPending.nbtTags, dataoutputstream); dataoutputstream.close(); } + + protected DataOutputStream getChunkOutputStream(AnvilChunkLoader.PendingChunk pending) + { + return RegionFileCache.getChunkOutputStream(this.chunkSaveLocation, pending.chunkCoordinate.chunkXPos, pending.chunkCoordinate.chunkZPos); + } public void saveExtraChunkData(World par1World, Chunk par2Chunk) {} @@ -267,7 +282,7 @@ // } } - private void writeChunkToNBT(Chunk par1Chunk, World par2World, NBTTagCompound par3NBTTagCompound) + protected void writeChunkToNBT(Chunk par1Chunk, World par2World, NBTTagCompound par3NBTTagCompound) { par3NBTTagCompound.setByte("V", (byte)1); par3NBTTagCompound.setInteger("xPos", par1Chunk.xPosition); @@ -394,7 +409,7 @@ } } - private Chunk readChunkFromNBT(World par1World, NBTTagCompound par2NBTTagCompound) + protected Chunk readChunkFromNBT(World par1World, NBTTagCompound par2NBTTagCompound) { int i = par2NBTTagCompound.getInteger("xPos"); int j = par2NBTTagCompound.getInteger("zPos"); @@ -533,7 +548,7 @@ } } - static class PendingChunk + protected static class PendingChunk { public final ChunkCoordIntPair chunkCoordinate; public final NBTTagCompound nbtTags; diff --git a/src/main/java/net/minecraft/world/storage/SaveHandler.java b/src/main/java/net/minecraft/world/storage/SaveHandler.java index 0786379..95ed034 100644 --- a/src/main/java/net/minecraft/world/storage/SaveHandler.java +++ b/src/main/java/net/minecraft/world/storage/SaveHandler.java @@ -49,7 +49,7 @@ this.setSessionLock(); } - private void setSessionLock() + protected void setSessionLock() { try { diff --git a/src/main/java/org/ultramine/commands/basic/TechCommands.java b/src/main/java/org/ultramine/commands/basic/TechCommands.java index d1a53fc..c274ab3 100644 --- a/src/main/java/org/ultramine/commands/basic/TechCommands.java +++ b/src/main/java/org/ultramine/commands/basic/TechCommands.java @@ -1,11 +1,14 @@ package org.ultramine.commands.basic; +import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import net.minecraft.command.CommandException; import net.minecraft.entity.Entity; import net.minecraft.entity.EnumCreatureType; import net.minecraft.entity.item.EntityItem; @@ -18,7 +21,6 @@ import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; -import static net.minecraft.util.EnumChatFormatting.*; import net.minecraft.world.WorldServer; import net.minecraft.world.chunk.Chunk; @@ -33,13 +35,18 @@ import org.ultramine.server.UltramineServerConfig; import org.ultramine.server.UltramineServerModContainer; import org.ultramine.server.BackupManager.BackupDescriptor; +import org.ultramine.server.WorldsConfig.WorldConfig; import org.ultramine.server.WorldsConfig.WorldConfig.Border; +import org.ultramine.server.WorldsConfig.WorldConfig.ImportFrom; import org.ultramine.server.chunk.ChunkProfiler; import org.ultramine.server.chunk.IChunkLoadCallback; import org.ultramine.server.chunk.OffHeapChunkStorage; import org.ultramine.server.util.BasicTypeParser; +import org.ultramine.server.util.GlobalExecutors; import org.ultramine.server.world.MultiWorld; import org.ultramine.server.world.WorldDescriptor; +import org.ultramine.server.world.WorldState; +import org.ultramine.server.world.imprt.ZipFileChunkLoader; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -48,6 +55,9 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import static net.minecraft.util.EnumChatFormatting.*; +import static org.ultramine.server.world.WorldState.*; + public class TechCommands { private static final Logger log = LogManager.getLogger(); @@ -159,7 +169,9 @@ permissions = {"command.technical.multiworld"}, syntax = { "[list]", - "[load unload hold goto destroy delete] <%world>" //No world validation + "[import] <%file>", + "[import] <%file> <%path>", + "[load unload hold destroy delete wipe unregister drop goto] <%world>" //No world validation } ) public static void multiworld(CommandContext ctx) @@ -171,10 +183,30 @@ ctx.sendMessage("command.multiworld.list.head"); for(WorldDescriptor desc : mw.getAllDescs()) { - ctx.sendMessage(GOLD, " - [%s](%s) - %s", desc.getDimension(), desc.getName(), desc.getState()); + ctx.sendMessage(GOLD, " - [%s](%s) - %s", desc.getDimension(), desc.getName(), worldStateColor(desc.getState())); } return; } + else if(ctx.getAction().equals("import")) + { + String filename = ctx.get("file").asString(); + String pathInArchive = ctx.contains("path") ? ctx.get("path").asString() : null; + File file = new File(ctx.getServer().getHomeDirectory(), filename); + if(!file.exists()) + throw new CommandException("command.multiworld.import.fail.nofile", filename); + if(file.isFile()) + ZipFileChunkLoader.checkZipFile(file, pathInArchive); + int dim = mw.allocTempDim(); + WorldDescriptor desc = mw.makeTempWorld(MultiWorld.getTempWorldName(dim)+"_"+file.getName(), dim); + WorldConfig config = desc.getConfig(); + config.importFrom = new ImportFrom(); + config.importFrom.file = filename; + config.importFrom.pathInArchive = pathInArchive; + config.generation.providerID = -10; + config.generation.disableModGeneration = true; + ctx.sendMessage("command.multiworld.import.success"); + return; + } WorldDescriptor desc = mw.getDescByNameOrID(ctx.get("world").asString()); @@ -186,21 +218,52 @@ if(desc.getState().isLoaded()) ctx.failure("command.multiworld.alreadyloaded"); - desc.forceLoad(); - ctx.sendMessage("command.multiworld.load.success"); + ctx.sendMessage("command.multiworld.load.start"); + handle(desc.forceLoadLater(), ctx, "command.multiworld.load.success", "command.multiworld.load.fail"); } else if(ctx.getAction().equals("unload")) { if(!desc.getState().isLoaded()) ctx.failure("command.multiworld.notloaded"); - desc.unload(); - ctx.sendMessage("command.multiworld.unload.success"); + ctx.sendMessage("command.multiworld.unload.start"); + handle(desc.unloadLater(true), ctx, "command.multiworld.unload.success", "command.multiworld.unload.fail"); } else if(ctx.getAction().equals("hold")) { - desc.hold(); - ctx.sendMessage("command.multiworld.hold.success"); + if(desc.getState() == HELD) + ctx.failure("command.multiworld.heldalready"); + ctx.sendMessage("command.multiworld.hold.start"); + handle(desc.holdLater(true), ctx, "command.multiworld.hold.success", "command.multiworld.hold.fail"); + } + else if(ctx.getAction().equals("destroy")) + { + if(!desc.getState().isLoaded()) + ctx.failure("command.multiworld.notloaded"); + + ctx.sendMessage("command.multiworld.destroy.start"); + handle(desc.holdLater(false), ctx, "command.multiworld.destroy.success", "command.multiworld.destroy.fail"); + + } + else if(ctx.getAction().equals("delete")) + { + ctx.sendMessage("command.multiworld.delete.start"); + handle(desc.deleteLater(), ctx, "command.multiworld.delete.success", "command.multiworld.delete.fail"); + } + else if(ctx.getAction().equals("wipe")) + { + ctx.sendMessage("command.multiworld.wipe.start"); + handle(desc.wipeLater(), ctx, "command.multiworld.wipe.success", "command.multiworld.wipe.fail"); + } + else if(ctx.getAction().equals("unregister")) + { + desc.unregister(); + ctx.sendMessage("command.multiworld.unregister.success"); + } + else if(ctx.getAction().equals("drop")) + { + desc.drop(); + ctx.sendMessage("command.multiworld.drop.success"); } else if(ctx.getAction().equals("goto")) { @@ -210,22 +273,23 @@ WorldServer world = desc.getWorld(); Teleporter.tpNow(ctx.getSenderAsPlayer(), desc.getDimension(), world.getWorldInfo().getSpawnX(), world.getWorldInfo().getSpawnY(), world.getWorldInfo().getSpawnZ()); } - else if(ctx.getAction().equals("destroy")) - { - if(!desc.getState().isLoaded()) - ctx.failure("command.multiworld.notloaded"); - - desc.destroyWorld(); - desc.hold(); - ctx.sendMessage("command.multiworld.destroy.success"); - - } - else if(ctx.getAction().equals("delete")) - { - desc.deleteWorld(); - desc.hold(); - ctx.sendMessage("command.multiworld.delete.success"); - } + } + + private static ChatComponentText worldStateColor(WorldState state) + { + ChatComponentText comp = new ChatComponentText(state.toString()); + comp.getChatStyle().setColor(state == UNREGISTERED ? DARK_RED : state == HELD ? RED : state == AVAILABLE ? YELLOW : state == LOADED ? DARK_GREEN : WHITE); + return comp; + } + + private static void handle(CompletableFuture future, CommandContext ctx, String success, String fail) + { + future.whenCompleteAsync((v, e) -> { + if(e == null) + ctx.sendMessage(success); + else + ctx.sendMessage(RED, RED, fail, e.toString()); + }, GlobalExecutors.nextTick()); } @Command( diff --git a/src/main/java/org/ultramine/server/BackupManager.java b/src/main/java/org/ultramine/server/BackupManager.java index c235a43..1088b8a 100644 --- a/src/main/java/org/ultramine/server/BackupManager.java +++ b/src/main/java/org/ultramine/server/BackupManager.java @@ -267,8 +267,8 @@ if(!moveOnly.contains(server.getMultiWorld().getSaveDirName(world))) continue; WorldDescriptor desc = server.getMultiWorld().getDescFromWorld(world); - List players = desc.extractPlayer(); - desc.destroyWorld(); + List players = desc.extractPlayers(); + desc.unloadNow(false); dimToPlayerMap.put(world.provider.dimensionId, players); } diff --git a/src/main/java/org/ultramine/server/UltramineServerModContainer.java b/src/main/java/org/ultramine/server/UltramineServerModContainer.java index d46c172..530e228 100644 --- a/src/main/java/org/ultramine/server/UltramineServerModContainer.java +++ b/src/main/java/org/ultramine/server/UltramineServerModContainer.java @@ -28,6 +28,8 @@ import org.ultramine.server.tools.ButtonCommand; import org.ultramine.server.tools.ItemBlocker; import org.ultramine.server.tools.WarpProtection; +import org.ultramine.server.util.GlobalExecutors; +import org.ultramine.server.util.SyncServerExecutor; import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; @@ -130,6 +132,7 @@ { if(e.getSide().isServer()) ConfigurationHandler.saveServerConfig(); + ((SyncServerExecutor)GlobalExecutors.nextTick()).register(); } catch (Throwable t) { diff --git a/src/main/java/org/ultramine/server/WorldsConfig.java b/src/main/java/org/ultramine/server/WorldsConfig.java index 965beac..75a65d8 100644 --- a/src/main/java/org/ultramine/server/WorldsConfig.java +++ b/src/main/java/org/ultramine/server/WorldsConfig.java @@ -12,6 +12,7 @@ { public int dimension; public String name; + public ImportFrom importFrom; public Generation generation; public MobSpawn mobSpawn; public Settings settings; @@ -20,6 +21,12 @@ public LoadBalancer loadBalancer; public Portals portals = new Portals(); + public static class ImportFrom + { + public String file; + public String pathInArchive; + } + public static class Generation { public String seed; diff --git a/src/main/java/org/ultramine/server/util/ConfigUtil.java b/src/main/java/org/ultramine/server/util/ConfigUtil.java new file mode 100644 index 0000000..1f1ec08 --- /dev/null +++ b/src/main/java/org/ultramine/server/util/ConfigUtil.java @@ -0,0 +1,72 @@ +package org.ultramine.server.util; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ConfigUtil +{ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static T deepClone(T obj) + { + if(obj == null) + return null; + Class cls = obj.getClass(); + if(cls.isPrimitive() || cls == String.class || cls == Boolean.class || obj instanceof Number || obj instanceof Enum) + return obj; + else if(obj instanceof List) + { + List orig = (List)obj; + List ret = new ArrayList(orig.size()); + for(Object o : orig) + ret.add(deepClone(o)); + return (T)ret; + } + else if(obj instanceof Set) + { + Set orig = (Set)obj; + Set ret = new HashSet(); + for(Object o : orig) + ret.add(deepClone(o)); + return (T)ret; + } + else if(obj instanceof Map) + { + Map orig = (Map)obj; + Map ret = new HashMap(); + for(Map.Entry ent : orig.entrySet()) + ret.put(deepClone(ent.getKey()), deepClone(ent.getValue())); + return (T)ret; + } + else if(cls.isArray()) + { + int len = Array.getLength(obj); + Object ret = Array.newInstance(cls.getComponentType(), len); + for(int i = 0; i < len; i++) + Array.set(ret, i, deepClone(Array.get(obj, i))); + return (T)ret; + } + else + { + try + { + Object ret = cls.newInstance(); + for(Field f : cls.getDeclaredFields()) + { + f.setAccessible(true); + f.set(ret, deepClone(f.get(obj))); + } + return (T)ret; + } + catch (Exception e) + { + throw new RuntimeException("Failed to clone object: "+obj, e); + } + } + } +} diff --git a/src/main/java/org/ultramine/server/util/GlobalExecutors.java b/src/main/java/org/ultramine/server/util/GlobalExecutors.java index 3cc62e6..637c0e6 100644 --- a/src/main/java/org/ultramine/server/util/GlobalExecutors.java +++ b/src/main/java/org/ultramine/server/util/GlobalExecutors.java @@ -1,5 +1,6 @@ package org.ultramine.server.util; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -9,6 +10,7 @@ { private static final ExecutorService io = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("UM IO #%d").setDaemon(true).build()); private static final ExecutorService cached = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setNameFormat("UM cached #%d").setDaemon(true).build()); + private static final Executor sync = new SyncServerExecutor(); /** * Обрабатывает задачи на сохранение чего-либо на диск/в БД. Используется @@ -29,4 +31,12 @@ { return cached; } + + /** + * Выполняет задачи в основном потоке сервера, на следующем тике + */ + public static Executor nextTick() + { + return sync; + } } diff --git a/src/main/java/org/ultramine/server/util/SyncServerExecutor.java b/src/main/java/org/ultramine/server/util/SyncServerExecutor.java new file mode 100644 index 0000000..11a9dd1 --- /dev/null +++ b/src/main/java/org/ultramine/server/util/SyncServerExecutor.java @@ -0,0 +1,41 @@ +package org.ultramine.server.util; + +import java.util.Queue; +import java.util.concurrent.Executor; + +import com.google.common.collect.Queues; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; + +public class SyncServerExecutor implements Executor +{ + private final Queue queue = Queues.newConcurrentLinkedQueue(); + + public void register() + { + FMLCommonHandler.instance().bus().register(this); + } + + public void unregister() + { + FMLCommonHandler.instance().bus().unregister(this); + } + + @Override + public void execute(Runnable toRun) + { + queue.add(toRun); + } + + @SubscribeEvent + public void onServerTick(TickEvent.ServerTickEvent e) + { + if(e.phase == TickEvent.Phase.END) + { + for(Runnable toRun; (toRun = queue.poll()) != null;) + toRun.run(); + } + } +} diff --git a/src/main/java/org/ultramine/server/util/ZipUtil.java b/src/main/java/org/ultramine/server/util/ZipUtil.java index 2c7eac4..8db6bfc 100644 --- a/src/main/java/org/ultramine/server/util/ZipUtil.java +++ b/src/main/java/org/ultramine/server/util/ZipUtil.java @@ -1,6 +1,5 @@ package org.ultramine.server.util; -import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.File; @@ -8,6 +7,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.Arrays; @@ -19,7 +19,6 @@ import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.apache.commons.io.IOUtils; @@ -96,14 +95,15 @@ public static void unzip(File zipfile, File outDir, Function filter) throws IOException { - ZipInputStream zip = null; + ZipFile zip = null; try { - zip = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipfile))); + zip = new ZipFile(zipfile); byte[] buffer = new byte[65536]; - for(ZipEntry ze = zip.getNextEntry(); ze != null; ze = zip.getNextEntry()) + for (Enumeration e = zip.entries(); e.hasMoreElements();) { + ZipEntry ze = e.nextElement(); String name = ze.getName(); if(filter != null) name = filter.apply(name); @@ -117,14 +117,16 @@ else { FileOutputStream fout = null; + InputStream inp = null; try { fout = new FileOutputStream(target); - IOUtils.copyLarge(zip, fout, buffer); + IOUtils.copyLarge(inp = zip.getInputStream(ze), fout, buffer); } finally { IOUtils.closeQuietly(fout); + IOUtils.closeQuietly(inp); } } } @@ -156,4 +158,18 @@ return set; } + + public static Set getRootFiles(ZipFile zip) + { + Set set = new HashSet(); + for (Enumeration e = zip.entries(); e.hasMoreElements();) + { + ZipEntry ze = e.nextElement(); + String name = ze.getName(); + if(ze.isDirectory()) + set.add(name.substring(0, name.indexOf('/'))); + } + + return set; + } } diff --git a/src/main/java/org/ultramine/server/world/MultiWorld.java b/src/main/java/org/ultramine/server/world/MultiWorld.java index 7b8f16e..a0b6a1e 100644 --- a/src/main/java/org/ultramine/server/world/MultiWorld.java +++ b/src/main/java/org/ultramine/server/world/MultiWorld.java @@ -28,6 +28,7 @@ import org.ultramine.server.ConfigurationHandler; import org.ultramine.server.WorldsConfig.WorldConfig; import org.ultramine.server.util.BasicTypeParser; +import org.ultramine.server.util.ConfigUtil; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.EventPriority; @@ -124,9 +125,9 @@ for(int dim : DimensionManager.getStaticDimensionIDs()) { WorldDescriptor desc = getOrCreateDescriptor(dim); - desc.setState(WorldState.UNLOADED); + desc.setState(WorldState.AVAILABLE); if(desc.getConfig() == null) - desc.setConfig(ConfigurationHandler.getWorldsConfig().global); + desc.setConfig(cloneGlobalConfig()); } // @@ -142,11 +143,11 @@ WorldDescriptor overDesc = getDescByID(0); if(overDesc == null) throw new RuntimeException("WorldDescriptor for OverWorld (dimension = 0) not found!"); - overDesc.weakLoad(); + overDesc.weakLoadNow(); for(WorldDescriptor desc : nameToWorldMap.values()) if(desc.getDimension() != 0) - desc.weakLoad(); + desc.weakLoadNow(); serverLoaded = true; } @@ -180,11 +181,11 @@ if(desc == null && DimensionManager.isDimensionRegistered(dim)) { desc = getOrCreateDescriptor(dim); - desc.setState(WorldState.UNLOADED); - desc.setConfig(ConfigurationHandler.getWorldsConfig().global); + desc.setState(WorldState.AVAILABLE); + desc.setConfig(cloneGlobalConfig()); } if(desc != null) - desc.weakLoad(); + desc.weakLoadNow(); } @SideOnly(Side.CLIENT) @@ -220,6 +221,12 @@ return conf; } + + @SideOnly(Side.SERVER) + static WorldConfig cloneGlobalConfig() + { + return ConfigUtil.deepClone(ConfigurationHandler.getWorldsConfig().global); + } private void checkDuplicates(List list) { @@ -268,6 +275,22 @@ nameToWorldMap.remove(oldName); nameToWorldMap.put(newName, desc); } + + /** @return A random string from "000000" to "zzzzzz" */ + @SideOnly(Side.SERVER) + private static String genRandomMark() + { + String rnd = Long.toString(System.nanoTime() % 0x81bf0fffL, Character.MAX_RADIX); + if(rnd.length() != 6) + return new StringBuilder("000000").replace(6-rnd.length(), 6, rnd).toString(); + return rnd; + } + + @SideOnly(Side.SERVER) + public static String getTempWorldName(int dim) + { + return "temp_"+genRandomMark()+"_"+dim; + } @SideOnly(Side.SERVER) public int allocTempDim() @@ -284,7 +307,7 @@ public WorldDescriptor makeTempWorld() { int dim = allocTempDim(); - return makeTempWorld("temp_"+dim, dim); + return makeTempWorld(getTempWorldName(dim), dim); } @SideOnly(Side.SERVER) @@ -300,7 +323,8 @@ throw new RuntimeException("WorldDescriptor for dimension "+dim+" already registered (on making temp world)"); WorldDescriptor desc = getOrCreateDescriptor(dim, name); - desc.setConfig(ConfigurationHandler.getWorldsConfig().global); + desc.setConfig(cloneGlobalConfig()); + desc.setTemp(true); return desc; } @@ -374,7 +398,7 @@ { WorldDescriptor desc = getDescByID(dim); if(desc == null) - return server.isSinglePlayer() ? getDefaultClientConfig(dim) : ConfigurationHandler.getWorldsConfig().global; + return server.isSinglePlayer() ? getDefaultClientConfig(dim) : cloneGlobalConfig(); return desc.getConfig(); } @@ -386,7 +410,7 @@ List dirs = new ArrayList(); for(WorldDescriptor desc : nameToWorldMap.values()) { - if(desc.getState() == WorldState.LOADED || desc.getState() == WorldState.UNLOADED) + if(!desc.isTemp() && desc.getDirectory().isDirectory()) dirs.add(desc.getName()); } @@ -436,4 +460,10 @@ dimToWorldMap.clear(); nameToWorldMap.clear(); } + + void dropDesc(WorldDescriptor desc) + { + dimToWorldMap.remove(desc.getDimension()); + nameToWorldMap.remove(desc.getName()); + } } diff --git a/src/main/java/org/ultramine/server/world/WorldDescriptor.java b/src/main/java/org/ultramine/server/world/WorldDescriptor.java index cf48fa8..3b5e1b8 100644 --- a/src/main/java/org/ultramine/server/world/WorldDescriptor.java +++ b/src/main/java/org/ultramine/server/world/WorldDescriptor.java @@ -5,6 +5,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.scoreboard.ScorePlayerTeam; @@ -12,15 +15,6 @@ import net.minecraft.tileentity.TileEntity; import net.minecraft.world.WorldManager; import net.minecraft.world.WorldServer; -import net.minecraft.world.WorldServerMulti; -import net.minecraft.world.WorldSettings; -import net.minecraft.world.WorldType; -import net.minecraft.world.chunk.storage.AnvilSaveHandler; -import net.minecraft.world.chunk.storage.RegionFileCache; -import net.minecraft.world.storage.ISaveFormat; -import net.minecraft.world.storage.ISaveHandler; -import net.minecraft.world.storage.ThreadedFileIOBase; -import net.minecraft.world.storage.WorldInfo; import net.minecraftforge.common.DimensionManager; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.world.WorldEvent; @@ -28,13 +22,18 @@ import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.ultramine.server.ConfigurationHandler; import org.ultramine.server.Teleporter; import org.ultramine.server.WorldsConfig.WorldConfig; import org.ultramine.server.WorldsConfig.WorldConfig.MobSpawn.MobSpawnEngine; import org.ultramine.server.WorldsConfig.WorldConfig.Settings.WorldTime; import org.ultramine.server.util.BasicTypeParser; +import org.ultramine.server.util.GlobalExecutors; import org.ultramine.server.util.WarpLocation; +import org.ultramine.server.world.load.IWorldLoader; +import org.ultramine.server.world.load.ImportWorldLoader; +import org.ultramine.server.world.load.OverworldLoader; +import org.ultramine.server.world.load.SplitedWorldLoader; +import org.ultramine.server.world.load.UnsplitedWorldLoader; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -52,6 +51,9 @@ private File directory; private WorldConfig config; private WorldState state = WorldState.UNREGISTERED; + private final AtomicBoolean transitState = new AtomicBoolean(); + private boolean temp; + private IWorldLoader worldLoader; private WorldServer world; public WorldDescriptor(MinecraftServer server, MultiWorld mw, boolean splitWorldDirs, int dimension, String name) @@ -111,6 +113,16 @@ this.state = state; } + public boolean isTemp() + { + return temp; + } + + public void setTemp(boolean temp) + { + this.temp = temp; + } + public WorldServer getWorld() { return world; @@ -127,7 +139,7 @@ public WorldServer getOrLoadWorld() { if(state != WorldState.LOADED) - weakLoad(); + weakLoadNow(); return world; } @@ -140,96 +152,57 @@ if(DimensionManager.isDimensionRegistered(dimension)) DimensionManager.unregisterDimension(dimension); DimensionManager.registerDimension(dimension, config.generation.providerID); - setState(WorldState.UNLOADED); + setState(WorldState.AVAILABLE); mw.sendDimensionToAll(dimension, config.generation.providerID); } @SideOnly(Side.SERVER) - public void forceLoad() + public void forceLoadNow() { if(state == WorldState.UNREGISTERED) register(); - load(); + loadNow(); } @SideOnly(Side.SERVER) - public void weakLoad() + public void weakLoadNow() { if(state == WorldState.HELD || state == WorldState.UNREGISTERED) return; - load(); + loadNow(); } @SideOnly(Side.SERVER) - private void load() + private void loadNow() { if(state.isLoaded()) throw new RuntimeException("Dimension ["+dimension+"] is already loaded"); - ISaveFormat format = server.getActiveAnvilConverter(); - if(config == null) - { - log.warn("World with dimension id:{} was loaded bypass worlds configuration. Using global config", dimension); - config = ConfigurationHandler.getWorldsConfig().global; - } - - WorldServer world; - if(dimension == 0) - { - ISaveHandler mainSaveHandler = format.getSaveLoader(name, true); - WorldInfo mainWorldInfo = mainSaveHandler.loadWorldInfo(); - WorldSettings mainSettings = makeSettings(mainWorldInfo, config); - - world = new WorldServer(server, mainSaveHandler, name, dimension, mainSettings, server.theProfiler); - } - else if(splitWorldDirs) - { - ISaveHandler save = format.getSaveLoader(name, false); - ((AnvilSaveHandler)save).setSingleStorage(); - world = new WorldServer(server, save, name, dimension, makeSettings(save.loadWorldInfo(), config), server.theProfiler); - } - else - { - WorldServer mainWorld = mw.getWorldByID(0); - ISaveHandler mainSaveHandler = mainWorld.getSaveHandler(); - WorldInfo mainWorldInfo = mainWorld.getWorldInfo(); - world = new WorldServerMulti(server, mainSaveHandler, mainWorldInfo.getWorldName(), dimension, makeSettings(mainWorldInfo, config), mainWorld, server.theProfiler); - } + worldLoader = createLoader(); + if(worldLoader.hasAsyncLoadPhase()) + worldLoader.doAsyncLoadPhase(); + WorldServer world = worldLoader.doLoad(); setWorld(world); initWorld(); } - @SideOnly(Side.SERVER) - private WorldSettings makeSettings(WorldInfo wi, WorldConfig conf) + private IWorldLoader createLoader() { - WorldSettings mainSettings; - - if (wi == null) + if(dimension == 0) + return new OverworldLoader(this, server); + if(config.importFrom != null) { - mainSettings = new WorldSettings(toSeed(conf.generation.seed), server.getGameType(), conf.generation.generateStructures, - server.isHardcore(), WorldType.parseWorldType(conf.generation.levelType)); - mainSettings.func_82750_a(conf.generation.generatorSettings); - } - else - { - mainSettings = new WorldSettings(wi); + File file = new File(server.getHomeDirectory(), config.importFrom.file); + if(!file.exists()) + throw new RuntimeException("File not found: "+file.getAbsolutePath()); + return new ImportWorldLoader(this, server); } - return mainSettings; - } - - private static long toSeed(String seedstr) - { - try - { - return Long.parseLong(seedstr); - } - catch (NumberFormatException e) - { - return seedstr.hashCode(); - } + if(!splitWorldDirs) + return new UnsplitedWorldLoader(this, server); + return new SplitedWorldLoader(this, server); } @SideOnly(Side.SERVER) @@ -257,10 +230,10 @@ ((WorldServer)world).theChunkProviderServer.setWorldUnloaded(); world = null; if(getState().isLoaded()) - setState(WorldState.UNLOADED); + setState(WorldState.AVAILABLE); } - public List extractPlayer() + public List extractPlayers() { if(!state.isLoaded()) return Collections.emptyList(); @@ -282,79 +255,6 @@ return players; } - @SideOnly(Side.SERVER) - public void hold() - { - if(getState().isLoaded()) - unload(); - setState(WorldState.HELD); - } - - @SideOnly(Side.SERVER) - public void unload() - { - if(!getState().isLoaded()) - return; - if(!world.playerEntities.isEmpty()) - movePlayersOut(); - - DimensionManager.unloadWorld(dimension); - } - - @SuppressWarnings("unchecked") - public void destroyWorld() - { - if(!getState().isLoaded()) - return; - if(!world.playerEntities.isEmpty()) - movePlayersOut(); - - WorldServer world = this.world; - if(world.provider.dimensionId == 0) - for(ScorePlayerTeam team : new ArrayList(world.getScoreboard().getTeams())) - world.getScoreboard().removeTeam(team); - - world.theChunkProviderServer.setWorldUnloaded(); - world.theChunkProviderServer.unloadAll(false); - world.forceUnloadTileEntities(); - - MinecraftForge.EVENT_BUS.post(new WorldEvent.Unload(world)); - DimensionManager.setWorld(world.provider.dimensionId, null); - world.theChunkProviderServer.free(); - for(Object o : world.loadedTileEntityList) - ((TileEntity)o).setWorldObj(null); - world.loadedTileEntityList.clear(); - dispose(); - } - - @SideOnly(Side.SERVER) - public void deleteWorld() - { - if(state.isLoaded()) - destroyWorld(); - else - dispose(); - - try - { - FileUtils.cleanDirectory(getDirectory()); - } - catch(IOException e) - { - throw new RuntimeException(e); - } - } - - private void dispose() - { - try - { - ThreadedFileIOBase.threadedIOInstance.waitForFinish(); - } catch (InterruptedException ignored){} - - RegionFileCache.clearRegionFileReferences(); - } - private void movePlayersOut() { WarpLocation spawn = server.getConfigurationManager().getDataLoader().getWarp("spawn"); @@ -376,4 +276,260 @@ } } } + + @SideOnly(Side.SERVER) + @SuppressWarnings("unchecked") + private void destroyWorld(boolean save) + { + if(!getState().isLoaded()) + return; + if(!world.playerEntities.isEmpty()) + movePlayersOut(); + + WorldServer world = this.world; + if(world.provider.dimensionId == 0) + for(ScorePlayerTeam team : new ArrayList(world.getScoreboard().getTeams())) + world.getScoreboard().removeTeam(team); + + world.theChunkProviderServer.setWorldUnloaded(); + world.theChunkProviderServer.unloadAll(save); + world.forceUnloadTileEntities(); + if(save) + world.saveOtherData(); + + MinecraftForge.EVENT_BUS.post(new WorldEvent.Unload(world)); + DimensionManager.setWorld(world.provider.dimensionId, null); + world.theChunkProviderServer.free(); + for(Object o : world.loadedTileEntityList) + ((TileEntity)o).setWorldObj(null); + world.loadedTileEntityList.clear(); + } + + private void dispose() + { + if(worldLoader != null) + worldLoader.dispose(); + worldLoader = null; + } + + private void clearWorldDir() + { + if(!getDirectory().exists()) + return; + + try + { + FileUtils.cleanDirectory(getDirectory()); + } + catch(IOException e) + { + throw new RuntimeException(e); + } + } + + @SideOnly(Side.SERVER) + public void unloadNow(boolean save) + { + if(!getState().isLoaded()) + return; + + checkTransition(); + destroyWorld(save); + dispose(); + } + + @SideOnly(Side.SERVER) + public void holdNow(boolean save) + { + if(getState() == WorldState.UNREGISTERED || getState() == WorldState.HELD) + return; + checkTransition(); + if(getState().isLoaded()) + unloadNow(save); + setState(WorldState.HELD); + } + + @SideOnly(Side.SERVER) + public void deleteNow() + { + checkTransition(); + if(state.isLoaded()) + destroyWorld(false); + + dispose(); + clearWorldDir(); + } + + @SideOnly(Side.SERVER) + public void unregister() + { + if(state == WorldState.UNREGISTERED) + return; + checkTransition(); + if(state.isLoaded()) + { + destroyWorld(true); + dispose(); + } + DimensionManager.unregisterDimension(dimension); + setState(WorldState.UNREGISTERED); + } + + @SideOnly(Side.SERVER) + public void drop() + { + unregister(); + mw.dropDesc(this); + } + + @SideOnly(Side.SERVER) + private static CompletableFuture execLater(Runnable toRun) + { + return CompletableFuture.runAsync(toRun, GlobalExecutors.nextTick()); + } + + @SideOnly(Side.SERVER) + private void checkTransition() + { + if(transitState.get()) + throw new IllegalStateException("World ["+dimension+"] is in transitional state"); + } + + @SideOnly(Side.SERVER) + private void startTransition() + { + if(!transitState.compareAndSet(false, true)) + throw new IllegalStateException("World ["+dimension+"] is in transitional state"); + } + + @SideOnly(Side.SERVER) + private CompletableFuture endTransition(CompletableFuture last) + { + return last.whenComplete((v, e) -> { + transitState.compareAndSet(true, false); + if(e != null) { + log.error("Error in world ["+dimension+"] state transition. Aborting", e); + propagate(e); + } + }); + } + + private static RuntimeException propagate(Throwable t) + { + if(t instanceof CompletionException) + throw (CompletionException) t; + throw new CompletionException(t); + } + + @SideOnly(Side.SERVER) + private CompletableFuture loadLater() + { + if(state.isLoaded()) + throw new RuntimeException("Dimension ["+dimension+"] is already loaded"); + + worldLoader = createLoader(); + if(!worldLoader.hasAsyncLoadPhase()) + { + WorldServer world = worldLoader.doLoad(); + setWorld(world); + initWorld(); + return CompletableFuture.completedFuture(null); + } + else + { + return CompletableFuture.runAsync(() -> worldLoader.doAsyncLoadPhase(), GlobalExecutors.cachedExecutor()).thenRunAsync(() -> { + WorldServer world = worldLoader.doLoad(); + + setWorld(world); + initWorld(); + }, GlobalExecutors.nextTick()); + } + } + + @SideOnly(Side.SERVER) + private CompletableFuture forceLoadLater0() + { + if(state == WorldState.UNREGISTERED) + register(); + return loadLater(); + } + + @SideOnly(Side.SERVER) + public CompletableFuture forceLoadLater() + { + startTransition(); + return endTransition(forceLoadLater0()); + } + + @SideOnly(Side.SERVER) + public CompletableFuture weakLoadLater() + { + if(state == WorldState.HELD || state == WorldState.UNREGISTERED) + return CompletableFuture.completedFuture(null); + + startTransition(); + return endTransition(loadLater()); + } + + @SideOnly(Side.SERVER) + private CompletableFuture downgradeLater(final boolean save, WorldState targetState) + { + if(getState().ordinal() >= targetState.ordinal()) + return CompletableFuture.completedFuture(null); + + if(!getState().isLoaded()) + { + setState(targetState); + return CompletableFuture.completedFuture(null); + } + + return execLater(() -> { + if(getState().isLoaded()) + destroyWorld(save); + setState(targetState); + }).thenRunAsync(() -> dispose(), GlobalExecutors.cachedExecutor()); + } + + @SideOnly(Side.SERVER) + public CompletableFuture unloadLater(final boolean save) + { + if(!getState().isLoaded()) + return CompletableFuture.completedFuture(null); + + startTransition(); + return endTransition(downgradeLater(save, WorldState.AVAILABLE)); + } + + @SideOnly(Side.SERVER) + public CompletableFuture holdLater(boolean save) + { + if(getState() == WorldState.UNREGISTERED || getState() == WorldState.HELD) + return CompletableFuture.completedFuture(null); + + startTransition(); + return endTransition(downgradeLater(save, WorldState.HELD)); + } + + @SideOnly(Side.SERVER) + private CompletableFuture deleteLater0() + { + if(state.isLoaded()) + return downgradeLater(false, WorldState.HELD).thenRunAsync(() -> clearWorldDir(), GlobalExecutors.cachedExecutor()); + else + return CompletableFuture.runAsync(() -> clearWorldDir(), GlobalExecutors.cachedExecutor()); + } + + @SideOnly(Side.SERVER) + public CompletableFuture deleteLater() + { + startTransition(); + return endTransition(deleteLater0()); + } + + @SideOnly(Side.SERVER) + public CompletableFuture wipeLater() + { + startTransition(); + return endTransition(deleteLater0().thenComposeAsync(v -> forceLoadLater0(), GlobalExecutors.nextTick())); + } } diff --git a/src/main/java/org/ultramine/server/world/WorldState.java b/src/main/java/org/ultramine/server/world/WorldState.java index 3517371..34b78a0 100644 --- a/src/main/java/org/ultramine/server/world/WorldState.java +++ b/src/main/java/org/ultramine/server/world/WorldState.java @@ -2,7 +2,7 @@ public enum WorldState { - LOADED(true), UNLOADED(false), HELD(false), UNREGISTERED(false); + LOADED(true), AVAILABLE(false), HELD(false), UNREGISTERED(false); private final boolean isLoaded; diff --git a/src/main/java/org/ultramine/server/world/imprt/DirectoryChunkLoader.java b/src/main/java/org/ultramine/server/world/imprt/DirectoryChunkLoader.java new file mode 100644 index 0000000..86bbcd6 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/imprt/DirectoryChunkLoader.java @@ -0,0 +1,23 @@ +package org.ultramine.server.world.imprt; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; + +public class DirectoryChunkLoader extends ImportChunkLoader +{ + private final File fromDir; + + public DirectoryChunkLoader(File tempDir, File fromDir) + { + super(tempDir); + this.fromDir = fromDir; + } + + @Override + protected void unpackFile(String name) throws IOException + { + FileUtils.copyFile(new File(fromDir, name), new File(tempDir, name)); + } +} diff --git a/src/main/java/org/ultramine/server/world/imprt/DirectorySaveHandler.java b/src/main/java/org/ultramine/server/world/imprt/DirectorySaveHandler.java new file mode 100644 index 0000000..fad5554 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/imprt/DirectorySaveHandler.java @@ -0,0 +1,51 @@ +package org.ultramine.server.world.imprt; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; + +import net.minecraft.server.MinecraftServer; + +public class DirectorySaveHandler extends ImportSaveHandler +{ + private File fromDir; + + protected DirectorySaveHandler(boolean tempDirExists, boolean tempDirEmpty, String dirname, File fromDir) + { + super(tempDirExists, tempDirEmpty, dirname); + this.fromDir = fromDir; + } + + public static DirectorySaveHandler create(String dirname, File dir) + { + File tempDir = new File(MinecraftServer.getServer().getWorldsDir(), dirname); + boolean exists = tempDir.exists(); + return new DirectorySaveHandler(exists, exists && tempDir.list().length == 0, dirname, dir); + } + + @Override + protected ImportChunkLoader createChunkLoader() throws IOException + { + return new DirectoryChunkLoader(tempDir, fromDir); + } + + @Override + protected void unpackExceptRegions() throws IOException + { + FileUtils.forceMkdir(tempDir); + if(!fromDir.isDirectory()) + throw new IOException(fromDir.getAbsolutePath() + " is not a directory!"); + for(File file : fromDir.listFiles()) + { + String name = file.getName(); + if(!name.equals("region") && !name.startsWith("DIM")) + { + if(file.isFile()) + FileUtils.copyFileToDirectory(file, tempDir); + else + FileUtils.copyDirectoryToDirectory(file, tempDir); + } + } + } +} diff --git a/src/main/java/org/ultramine/server/world/imprt/ImportChunkLoader.java b/src/main/java/org/ultramine/server/world/imprt/ImportChunkLoader.java new file mode 100644 index 0000000..6cbc1d4 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/imprt/ImportChunkLoader.java @@ -0,0 +1,119 @@ +package org.ultramine.server.world.imprt; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import net.minecraft.world.MinecraftException; +import net.minecraft.world.World; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import net.minecraft.world.chunk.storage.RegionFile; +import net.minecraft.world.storage.ThreadedFileIOBase; + +public abstract class ImportChunkLoader extends AnvilChunkLoader +{ + private static final Logger log = LogManager.getLogger(); + protected final TIntObjectMap regionCache = new TIntObjectHashMap(); + protected final File tempDir; + protected volatile boolean closed; + + protected ImportChunkLoader(File tempDir) + { + super(tempDir); + this.tempDir = tempDir; + } + + protected abstract void unpackFile(String name) throws IOException; + + private String getRegionFileName(int cx, int cz) + { + return "region/r."+(cx >> 5) + "." + (cz >> 5) + ".mca"; + } + + private synchronized RegionFile getRegion(int cx, int cz) + { + int x = cx >> 5; + int z = cz >> 5; + int key = (x & 0xffff) | ((z & 0xffff) << 11); + RegionFile region = regionCache.get(key); + if(region == null) + { + clearCache(128); + String name = getRegionFileName(cx, cz); + File regFile = new File(tempDir, name); + if(!regFile.exists()) + { + try { + unpackFile(name); + } catch(IOException e) { + log.error("Error unpacking RegionFile: "+name, e); + } + } + region = new RegionFile(regFile); + regionCache.put(key, region); + } + return region; + } + + private void clearCache(int limit) + { + if(regionCache.size() > limit) + { + try { + ThreadedFileIOBase.threadedIOInstance.waitForFinish(); + } catch (InterruptedException interruptedexception) {} + + for(RegionFile region : regionCache.valueCollection()) + try{region.close();}catch(IOException igrored){} + regionCache.clear(); + } + } + + @Override + protected boolean isChunkExistsInFile(int cx, int cz) + { + return getRegion(cx, cz).chunkExists(cx & 31, cz & 31); + } + + @Override + protected DataInputStream getChunkInputStream(int cx, int cz) + { + return getRegion(cx, cz).getChunkDataInputStream(cx & 31, cz & 31); + } + + @Override + protected DataOutputStream getChunkOutputStream(AnvilChunkLoader.PendingChunk pending) + { + return getRegion(pending.chunkCoordinate.chunkXPos, pending.chunkCoordinate.chunkZPos) + .getChunkDataOutputStream(pending.chunkCoordinate.chunkXPos & 31, pending.chunkCoordinate.chunkZPos & 31); + } + + @Override + public void saveChunk(World world, Chunk chunk) throws MinecraftException, IOException + { + if(!tempDir.exists() || closed) + return; + super.saveChunk(world, chunk); + } + + @Override + public boolean writeNextIO() + { + if(!tempDir.exists() || closed) + return false; + return super.writeNextIO(); + } + + public synchronized void close() + { + clearCache(0); + closed = true; + } +} diff --git a/src/main/java/org/ultramine/server/world/imprt/ImportSaveHandler.java b/src/main/java/org/ultramine/server/world/imprt/ImportSaveHandler.java new file mode 100644 index 0000000..afaaa1a --- /dev/null +++ b/src/main/java/org/ultramine/server/world/imprt/ImportSaveHandler.java @@ -0,0 +1,108 @@ +package org.ultramine.server.world.imprt; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.MinecraftException; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.chunk.storage.AnvilSaveHandler; +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.storage.WorldInfo; + +public abstract class ImportSaveHandler extends AnvilSaveHandler +{ + private static final Logger log = LogManager.getLogger(); + protected final File tempDir; + + protected ImportChunkLoader loader; + + protected ImportSaveHandler(boolean tempDirExists, boolean tempDirEmpty, String dirname) + { + super(MinecraftServer.getServer().getWorldsDir(), dirname, false); + tempDir = getWorldDirectory(); + if(tempDir.exists()) + { + if(!tempDirExists) + deleteTempDir(); + else if(tempDirEmpty) + cleanTempDir(); + } + } + + protected void deleteTempDir() + { + try { + FileUtils.deleteDirectory(tempDir); + } catch(IOException e) { + log.error("Failed to delete directory: " + tempDir.getAbsolutePath(), e); + } + } + + protected void cleanTempDir() + { + try { + FileUtils.cleanDirectory(tempDir); + } catch(IOException e) { + log.error("Failed to delete directory: " + tempDir.getAbsolutePath(), e); + } + } + + @Override + public IChunkLoader getChunkLoader(WorldProvider provider) + { + if(loader != null) + throw new IllegalStateException("Already loaded"); + + try + { + unpackIfNecessary(); + + return loader = createChunkLoader(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + protected abstract ImportChunkLoader createChunkLoader() throws IOException; + + public boolean shouldUnpack() + { + return !tempDir.exists() || tempDir.list().length == 0; + } + + public void unpackIfNecessary() throws IOException + { + if(shouldUnpack()) + unpackExceptRegions(); + } + + protected abstract void unpackExceptRegions() throws IOException; + + public void close() + { + if(loader != null) + loader.close(); + loader = null; + } + + @Override + public void flush() + { + close(); + } + + @Override protected void setSessionLock(){} + @Override public void checkSessionLock() throws MinecraftException{} + @Override public void saveWorldInfoWithPlayer(WorldInfo p_75755_1_, NBTTagCompound p_75755_2_){} + @Override public void saveWorldInfo(WorldInfo p_75761_1_){} + @Override public void writePlayerData(EntityPlayer p_75753_1_){} +} diff --git a/src/main/java/org/ultramine/server/world/imprt/ZipFileChunkLoader.java b/src/main/java/org/ultramine/server/world/imprt/ZipFileChunkLoader.java new file mode 100644 index 0000000..f5cc755 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/imprt/ZipFileChunkLoader.java @@ -0,0 +1,75 @@ +package org.ultramine.server.world.imprt; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.FileUtils; +import org.ultramine.server.util.ZipUtil; + +public class ZipFileChunkLoader extends ImportChunkLoader +{ + private final ZipFile zip; + private final String path; + + public ZipFileChunkLoader(File tempDir, File file, String path) throws ZipException, IOException + { + super(tempDir); + this.zip = new ZipFile(file); + checkZipFile(zip, path); + if(!path.isEmpty() && !path.endsWith("/")) + path += "/"; + this.path = path; + } + + @Override + protected void unpackFile(String name) throws IOException + { + ZipEntry ent = zip.getEntry(path+name); + if(ent == null) + return; + InputStream is = null; + try + { + is = zip.getInputStream(ent); + FileUtils.copyInputStreamToFile(is, new File(tempDir, name)); + } + finally + { + IOUtils.closeQuietly(is); + } + } + + @Override + public synchronized void close() + { + IOUtils.closeQuietly(zip); + super.close(); + } + + public static void checkZipFile(ZipFile zip, String path) + { + Set roots = ZipUtil.getRootFiles(zip); + if(!path.isEmpty() && !roots.contains(path) || path.isEmpty() && !roots.contains("region")) + throw new RuntimeException("Path not found in zip hierarchy: " + path); + } + + public static void checkZipFile(File file, String path) + { + ZipFile zip = null; + try + { + checkZipFile(zip = new ZipFile(file), path); + } catch(IOException e) { + throw new RuntimeException("Failed to open zip archive: "+file.getAbsolutePath(), e); + } finally { + if(zip != null) + try{zip.close();}catch(IOException ignored){} + } + } +} diff --git a/src/main/java/org/ultramine/server/world/imprt/ZipFileSaveHandler.java b/src/main/java/org/ultramine/server/world/imprt/ZipFileSaveHandler.java new file mode 100644 index 0000000..9bd5315 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/imprt/ZipFileSaveHandler.java @@ -0,0 +1,59 @@ +package org.ultramine.server.world.imprt; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.ultramine.server.util.ZipUtil; + +import com.google.common.base.Function; + +import net.minecraft.server.MinecraftServer; + +public class ZipFileSaveHandler extends ImportSaveHandler +{ + private final File file; + private final String path; + + private ZipFileSaveHandler(boolean tempDirExists, boolean tempDirEmpty, String dirname, File file, String path) + { + super(tempDirExists, tempDirEmpty, dirname); + this.file = file; + this.path = path; + ZipFileChunkLoader.checkZipFile(file, path); + } + + public static ZipFileSaveHandler create(String dirname, File file, String path) + { + File tempDir = new File(MinecraftServer.getServer().getWorldsDir(), dirname); + boolean exists = tempDir.exists(); + return new ZipFileSaveHandler(exists, exists && tempDir.list().length == 0, dirname, file, path); + } + + @Override + protected ImportChunkLoader createChunkLoader() throws IOException + { + return new ZipFileChunkLoader(tempDir, file, path); + } + + @Override + protected void unpackExceptRegions() throws IOException + { + FileUtils.forceMkdir(tempDir); + final String pathstart = path+"/"; + final String exclude1 = path+"/region"; + final String exclude2 = path+"/DIM"; + ZipUtil.unzip(file, tempDir, new Function() + { + @Override + public String apply(String name) + { + if(!name.startsWith(pathstart)) + return null; + if(name.startsWith(exclude1) || name.startsWith(exclude2)) + return null; + return name.substring(pathstart.length()); + } + }); + } +} diff --git a/src/main/java/org/ultramine/server/world/load/AbstractWorldLoader.java b/src/main/java/org/ultramine/server/world/load/AbstractWorldLoader.java new file mode 100644 index 0000000..1c89425 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/load/AbstractWorldLoader.java @@ -0,0 +1,73 @@ +package org.ultramine.server.world.load; + +import org.ultramine.server.WorldsConfig.WorldConfig; +import org.ultramine.server.world.WorldDescriptor; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.WorldType; +import net.minecraft.world.chunk.storage.RegionFileCache; +import net.minecraft.world.storage.WorldInfo; + +public abstract class AbstractWorldLoader implements IWorldLoader +{ + protected final WorldDescriptor desc; + protected final MinecraftServer server; + + protected AbstractWorldLoader(WorldDescriptor desc, MinecraftServer server) + { + this.desc = desc; + this.server = server; + } + + @Override + public boolean hasAsyncLoadPhase() + { + return false; + } + + @Override + public void doAsyncLoadPhase() + { + + } + + @Override + public void dispose() + { + RegionFileCache.clearRegionFileReferences(); + } + + @SideOnly(Side.SERVER) + protected WorldSettings makeSettings(WorldInfo wi, WorldConfig conf) + { + WorldSettings mainSettings; + + if (wi == null) + { + mainSettings = new WorldSettings(toSeed(conf.generation.seed), server.getGameType(), conf.generation.generateStructures, + server.isHardcore(), WorldType.parseWorldType(conf.generation.levelType)); + mainSettings.func_82750_a(conf.generation.generatorSettings); + } + else + { + mainSettings = new WorldSettings(wi); + } + + return mainSettings; + } + + protected static long toSeed(String seedstr) + { + try + { + return Long.parseLong(seedstr); + } + catch (NumberFormatException e) + { + return seedstr.hashCode(); + } + } +} diff --git a/src/main/java/org/ultramine/server/world/load/IWorldLoader.java b/src/main/java/org/ultramine/server/world/load/IWorldLoader.java new file mode 100644 index 0000000..fda9a90 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/load/IWorldLoader.java @@ -0,0 +1,14 @@ +package org.ultramine.server.world.load; + +import net.minecraft.world.WorldServer; + +public interface IWorldLoader +{ + boolean hasAsyncLoadPhase(); + + void doAsyncLoadPhase(); + + WorldServer doLoad(); + + void dispose(); +} diff --git a/src/main/java/org/ultramine/server/world/load/ImportWorldLoader.java b/src/main/java/org/ultramine/server/world/load/ImportWorldLoader.java new file mode 100644 index 0000000..440f391 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/load/ImportWorldLoader.java @@ -0,0 +1,65 @@ +package org.ultramine.server.world.load; + +import java.io.File; +import java.io.IOException; + +import org.ultramine.server.WorldsConfig.WorldConfig; +import org.ultramine.server.world.WorldDescriptor; +import org.ultramine.server.world.imprt.DirectorySaveHandler; +import org.ultramine.server.world.imprt.ImportSaveHandler; +import org.ultramine.server.world.imprt.ZipFileSaveHandler; + +import net.minecraft.server.MinecraftServer; + +public class ImportWorldLoader extends SplitedWorldLoader +{ + private ImportSaveHandler saveHandler; + + public ImportWorldLoader(WorldDescriptor desc, MinecraftServer server) + { + super(desc, server); + } + + @Override + protected ImportSaveHandler getSaveHandler() + { + if(saveHandler != null) + return saveHandler; + WorldConfig config = desc.getConfig(); + if(config.importFrom == null) + throw new RuntimeException("config.importFrom == null"); + + File file = new File(server.getHomeDirectory(), config.importFrom.file); + if(!file.exists()) + throw new RuntimeException("File not found: "+file.getAbsolutePath()); + if(file.isDirectory()) + return saveHandler = DirectorySaveHandler.create(desc.getName(), file); + else + return saveHandler = ZipFileSaveHandler.create(desc.getName(), file, config.importFrom.pathInArchive); + } + + @Override + public boolean hasAsyncLoadPhase() + { + return getSaveHandler().shouldUnpack(); + } + + @Override + public void doAsyncLoadPhase() + { + try + { + saveHandler.unpackIfNecessary(); + } + catch(IOException e) + { + throw new RuntimeException(e); + } + } + + @Override + public void dispose() + { + saveHandler.close(); + } +} diff --git a/src/main/java/org/ultramine/server/world/load/OverworldLoader.java b/src/main/java/org/ultramine/server/world/load/OverworldLoader.java new file mode 100644 index 0000000..d57d40f --- /dev/null +++ b/src/main/java/org/ultramine/server/world/load/OverworldLoader.java @@ -0,0 +1,29 @@ +package org.ultramine.server.world.load; + +import org.ultramine.server.world.WorldDescriptor; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; +import net.minecraft.world.WorldSettings; +import net.minecraft.world.storage.ISaveFormat; +import net.minecraft.world.storage.ISaveHandler; +import net.minecraft.world.storage.WorldInfo; + +public class OverworldLoader extends AbstractWorldLoader +{ + public OverworldLoader(WorldDescriptor desc, MinecraftServer server) + { + super(desc, server); + } + + @Override + public WorldServer doLoad() + { + ISaveFormat format = server.getActiveAnvilConverter(); + ISaveHandler mainSaveHandler = format.getSaveLoader(desc.getName(), true); + WorldInfo mainWorldInfo = mainSaveHandler.loadWorldInfo(); + WorldSettings mainSettings = makeSettings(mainWorldInfo, desc.getConfig()); + + return new WorldServer(server, mainSaveHandler, desc.getName(), desc.getDimension(), mainSettings, server.theProfiler); + } +} diff --git a/src/main/java/org/ultramine/server/world/load/SplitedWorldLoader.java b/src/main/java/org/ultramine/server/world/load/SplitedWorldLoader.java new file mode 100644 index 0000000..d26fea4 --- /dev/null +++ b/src/main/java/org/ultramine/server/world/load/SplitedWorldLoader.java @@ -0,0 +1,29 @@ +package org.ultramine.server.world.load; + +import org.ultramine.server.world.WorldDescriptor; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.storage.AnvilSaveHandler; +import net.minecraft.world.storage.ISaveHandler; + +public class SplitedWorldLoader extends AbstractWorldLoader +{ + public SplitedWorldLoader(WorldDescriptor desc, MinecraftServer server) + { + super(desc, server); + } + + @Override + public WorldServer doLoad() + { + ISaveHandler save = getSaveHandler(); + ((AnvilSaveHandler)save).setSingleStorage(); + return new WorldServer(server, save, desc.getName(), desc.getDimension(), makeSettings(save.loadWorldInfo(), desc.getConfig()), server.theProfiler); + } + + protected ISaveHandler getSaveHandler() + { + return server.getActiveAnvilConverter().getSaveLoader(desc.getName(), false); + } +} diff --git a/src/main/java/org/ultramine/server/world/load/UnsplitedWorldLoader.java b/src/main/java/org/ultramine/server/world/load/UnsplitedWorldLoader.java new file mode 100644 index 0000000..3dac00d --- /dev/null +++ b/src/main/java/org/ultramine/server/world/load/UnsplitedWorldLoader.java @@ -0,0 +1,34 @@ +package org.ultramine.server.world.load; + +import org.ultramine.server.world.WorldDescriptor; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; +import net.minecraft.world.WorldServerMulti; +import net.minecraft.world.storage.ISaveHandler; +import net.minecraft.world.storage.WorldInfo; + +public class UnsplitedWorldLoader extends AbstractWorldLoader +{ + public UnsplitedWorldLoader(WorldDescriptor desc, MinecraftServer server) + { + super(desc, server); + } + + @Override + public WorldServer doLoad() + { + WorldServer mainWorld = server.getMultiWorld().getWorldByID(0); + ISaveHandler mainSaveHandler = mainWorld.getSaveHandler(); + WorldInfo mainWorldInfo = mainWorld.getWorldInfo(); + return new WorldServerMulti( + server, + mainSaveHandler, + mainWorldInfo.getWorldName(), + desc.getDimension(), + makeSettings(mainWorldInfo, desc.getConfig()), + mainWorld, + server.theProfiler + ); + } +} diff --git a/src/main/resources/assets/ultramine/lang/en_US.lang b/src/main/resources/assets/ultramine/lang/en_US.lang index cfaa800..997323b 100644 --- a/src/main/resources/assets/ultramine/lang/en_US.lang +++ b/src/main/resources/assets/ultramine/lang/en_US.lang @@ -233,17 +233,32 @@ command.memstat.usage=/memstat command.memstat.description=Displays max, current allocated and free memory -command.multiworld.usage=/multiworld or /multiworld list +command.multiworld.usage=/multiworld or /multiworld list or /multiworld import [path] command.multiworld.description=All multiworld commands +command.multiworld.list.head=Dimension list: command.multiworld.alreadyloaded=Dimension is already loaded command.multiworld.notregistered=Dimension is not registered (not exists) command.multiworld.notloaded=Dimension is not loaded +command.multiworld.load.start=Starting dimension loading command.multiworld.load.success=Dimension successfuly loaded +command.multiworld.load.fail=Failed to load dimension +command.multiworld.unload.start=Starting dimension unloading command.multiworld.unload.success=Dimension successfuly unloaded -command.multiworld.hold.success=Dimension successfuly unloaded and held +command.multiworld.unload.fail=Failed to unload dimension +command.multiworld.hold.start=Starting dimension holding +command.multiworld.hold.success=Dimension successfuly held +command.multiworld.hold.fail=Failed to hold dimension +command.multiworld.destroy.start=Starting dimension destroying command.multiworld.destroy.success=Dimension successfuly destroyed +command.multiworld.destroy.fail=Failed to destroy dimension +command.multiworld.delete.start=Starting dimension deleting command.multiworld.delete.success=Dimension successfuly deleted -command.multiworld.list.head=Dimension list: +command.multiworld.delete.fail=Failed to delete dimension +command.multiworld.wipe.start=Starting dimension wiping +command.multiworld.wipe.success=Dimension successfuly wiped +command.multiworld.wipe.fail=Failed to wipe dimension +command.multiworld.unregister.success=Dimension successfuly unregistered +command.multiworld.drop.success=Dimension successfuly dropped command.countentity.usage=/countentity command.countentity.description=Specifies the number of Entity radially diff --git a/src/main/resources/assets/ultramine/lang/ru_RU.lang b/src/main/resources/assets/ultramine/lang/ru_RU.lang index 15b298b..6655667 100644 --- a/src/main/resources/assets/ultramine/lang/ru_RU.lang +++ b/src/main/resources/assets/ultramine/lang/ru_RU.lang @@ -233,17 +233,32 @@ command.memstat.usage=/memstat command.memstat.description=Displays max, current allocated and free memory -command.multiworld.usage=/multiworld <мир> ИЛИ /multiworld list +command.multiworld.usage=/multiworld <мир> ИЛИ /multiworld list ИЛИ /multiworld import <файл> [путь] command.multiworld.description=Все команды MultiWorld +command.multiworld.list.head=Список измерений: command.multiworld.alreadyloaded=Измерение уже загружено command.multiworld.notregistered=Измерение на зарегистрировано (не существует) command.multiworld.notloaded=Измерение не загружено +command.multiworld.load.start=Начата загрузка измерения command.multiworld.load.success=Измерение загружено +command.multiworld.load.fail=Не удалось загрузить измерение +command.multiworld.unload.start=Начата выгрузка измерения command.multiworld.unload.success=Измерение выгружено -command.multiworld.hold.success=Измерение выгружено и заморожено -command.multiworld.destroy.success=Измерение разрушено +command.multiworld.unload.fail=Не удалось выгрузить измерение +command.multiworld.hold.start=Начата блокировка измерения +command.multiworld.hold.success=Измерение выгружено и заблокировано +command.multiworld.hold.fail=Не удалось блокировать измерение +command.multiworld.destroy.start=Начата выгрузка измерения без сохранения +command.multiworld.destroy.success=Измерение выгружено +command.multiworld.destroy.fail=Не удалось выгрузить измерение +command.multiworld.delete.start=Начато удаление измерение command.multiworld.delete.success=Измерение удалено -command.multiworld.list.head=Список измерений: +command.multiworld.delete.fail=Не удалось удалить измерение +command.multiworld.wipe.start=Начат вайп измерение +command.multiworld.wipe.success=Измерение вайпнуто +command.multiworld.wipe.fail=Не удалось вайпнуть измерение +command.multiworld.unregister.success=Измерение разрегистрировано +command.multiworld.drop.success=Измерение удалено из конфигурации command.countentity.usage=/countentity <радиус> command.countentity.description=Подсчитывает количество Entity в радиусе