MCA file changed. But the world wan't changed

Discussion in 'Spigot Plugin Development' started by Toshimichi, Jul 17, 2018.

  1. I heard that MCA files in region directory affects all the chunks.
    So, I made a plugin which stores level.dat and MCA files and restores when needed.
    The plugin seemed to work well but in fact didn't work.
    I confirmed that the house_1 world has the same MCA files as house_2 world.
    However, the view from the house_2 world was different from that of house_1.

    • Code
    SimpleHouseWorld
    Code (Java):

    package net.hacbase.house.world;

    import net.hacbase.house.HouseAPI;
    import net.minecraft.server.v1_12_R1.DataConverter;
    import org.apache.commons.io.FileUtils;
    import org.bukkit.Bukkit;
    import org.bukkit.World;
    import org.bukkit.WorldCreator;
    import org.bukkit.entity.Player;
    import org.bukkit.util.FileUtil;

    import javax.xml.bind.DatatypeConverter;
    import java.io.*;
    import java.nio.ByteBuffer;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;

    public class SimpleHouseWorld implements HouseWorld {

        private static final int SPLIT = 30;
        private World world;

        public SimpleHouseWorld(World world) {
            this.world = world;
        }

        @Override
        public World getWorld() {
            return world;
        }

        @Override
        public HouseSnapshot store() throws IOException {
            world.save();
            try(ByteArrayOutputStream bOut = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(bOut)) {
                out.flush();
                try {
                    System.out.println("level.dat: " + DatatypeConverter.printHexBinary(
                            MessageDigest.getInstance("SHA-1").digest(FileUtils.readFileToByteArray(getLevelFile()))));
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                }
                out.writeObject(FileUtils.readFileToByteArray(getLevelFile()));
                if (!getRegionDir().isDirectory()) throw new FileNotFoundException("regionディレクトリが存在しません");
                File[] list = getRegionDir().listFiles();
                if (list == null) list = new File[0];
                out.writeInt(list.length);
                for(File mcr : list) {
                    out.writeUTF(mcr.getName());
                    try {
                        System.out.println(mcr.getName() + ": " + DatatypeConverter.printHexBinary(
                                MessageDigest.getInstance("SHA-1").digest(FileUtils.readFileToByteArray(mcr))));
                    } catch (NoSuchAlgorithmException e) {
                        e.printStackTrace();
                    }
                    out.writeObject(FileUtils.readFileToByteArray(mcr));
                }
                return new IOHouseSnapshot(bOut.toByteArray());

            }
        }

        @Override
        public void restore(HouseSnapshot snapshot) throws IOException {

            byte[] level;
            int size;
            Map<String, byte[]> mcr = new HashMap<>();
            try(ObjectInputStream in = new ObjectInputStream(snapshot.get())) {
                level = (byte[])in.readObject();
                try {
                    System.out.println("level.dat: " + DatatypeConverter.printHexBinary(
                            MessageDigest.getInstance("SHA-1").digest(level)));
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                }
                size = in.readInt();
                for(int i = 0; i < size; i++) {
                    String name = in.readUTF();
                    byte[] obj = (byte[])in.readObject();
                    System.out.println(name + ": " + DatatypeConverter.printHexBinary(
                            MessageDigest.getInstance("SHA-1").digest(obj)));
                    mcr.put(name, obj);
                }
            } catch (Exception e) {
                throw new IOException(e);
            }

            //Teleporting players temporarily
            List<Player> teleported = world.getPlayers();
            for (Player player : teleported) {
                player.teleport(Bukkit.getWorlds().get(0).getSpawnLocation());
            }

            //Unloading the world
            Bukkit.getScheduler().runTaskLater(HouseAPI.getAPI().getPlugin(), () -> Bukkit.unloadWorld(world, false), 10);

            Bukkit.getScheduler().runTaskLater(HouseAPI.getAPI().getPlugin(), () -> {

                //Overwrite the files
                try {
                    FileUtils.forceDelete(getLevelFile());
                    FileUtils.forceDelete(getRegionDir());
                    if (!getLevelFile().exists() && !getLevelFile().createNewFile())
                        throw new IOException("level.datファイルが作成できませんでした");
                    FileUtils.writeByteArrayToFile(getLevelFile(), level);
                    if (!getRegionDir().exists() && !getRegionDir().mkdirs())
                        throw new IOException("regionディレクトリが作成できませんでした");
                    for (Map.Entry<String, byte[]> entry : mcr.entrySet()) {
                        File file = new File(getRegionDir(), entry.getKey());
                        if (!file.exists() && !file.createNewFile())
                            throw new IOException("mcrファイルが作成できませんでした: " + file.getName());
                        FileUtils.writeByteArrayToFile(file, entry.getValue());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

                //Loading the  world
                this.world = new WorldCreator(world.getName()).generator(new VoidChunkGenerator()).createWorld();
                for (Player player : teleported) {
                    player.teleport(world.getSpawnLocation());
                }

            }, 40);
        }

        private File getRegionDir() {
            return new File(world.getWorldFolder(), "region");
        }

        private File getLevelFile() {
            return new File(world.getWorldFolder(), "level.dat");
        }
    }
     
    IOSnapshot
    Code (Java):
    package net.hacbase.house.world;

    import org.apache.commons.io.IOUtils;

    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Base64;

    public class IOHouseSnapshot implements HouseSnapshot {

        private final byte[] data;

        public IOHouseSnapshot(InputStream in) throws IOException {
            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                IOUtils.copy(in, out);
                data = out.toByteArray();
            }
        }

        public IOHouseSnapshot(byte[] data) {
            this.data = data;
        }

        @Override
        public InputStream get() {
            return new ByteArrayInputStream(data);
        }
    }
    • The output(SHA-1)
    Saving
    Code (Text):
    [14:43:45 INFO]: Toshimichi0915 issued server command: /save_house test
    [14:43:45 INFO]: level.dat: 3BD67BEB71059522F0E6F949AF9EB2E720CFC7E6
    [14:43:45 INFO]: r.0.0.mca: 3A29BB94382448F2F7144DF1CCEFCBE7416B0B7C
    [14:43:45 INFO]: r.-1.-1.mca: 3FB3C2CAAB020C272699984133E05EEDF63D00EA
    [14:43:45 INFO]: r.-1.0.mca: BCFFE82F5A91DAAE73D17C7BFA0CBA659B735DF9
    [14:43:45 INFO]: r.0.-1.mca: 222389221B3383906408BCC846A76EF02600FCAD
     
    Loading
    Code (Text):
    [14:43:58 INFO]: Toshimichi0915 issued server command: /go_house test
    [14:43:58 INFO]: level.dat: 3BD67BEB71059522F0E6F949AF9EB2E720CFC7E6
    [14:43:58 INFO]: r.0.0.mca: 3A29BB94382448F2F7144DF1CCEFCBE7416B0B7C
    [14:43:58 INFO]: r.-1.-1.mca: 3FB3C2CAAB020C272699984133E05EEDF63D00EA
    [14:43:58 INFO]: r.-1.0.mca: BCFFE82F5A91DAAE73D17C7BFA0CBA659B735DF9
    [14:43:58 INFO]: r.0.-1.mca: 222389221B3383906408BCC846A76EF02600FCAD
     
    • md5sum check
    The upper ones are MCA files in the house_2 world(Save)
    The lower ones are MCA files in the house_1 world(Load)
    [​IMG]

    • Views
    [​IMG]
    [​IMG]
    [​IMG]
     
  2. I would not expect world.save() to block so chances are you are not flushing the chunks to disk before you are copying the files.

    I have a feeling that as with world unloading there is API/event for knowing when the save operation has completed :/.
     
  3. I generated some delay after World#save.
    However, The situation did not change at all.
    Actually, it seemed that two worlds exist in memory
    because the files were completely changed and the files were also overwritten when I shut down the server.