Minecraft の Entity を ListUp

Minecraft JE (1.20) で遊んでいますが、迂闊にもエリトラで飛んでしまい、初めて会って着いてきていた ***Allay*** が居なくなってしまいました。
飛び立ったところに戻って見ましたが見つからず…。
ネットで検索しても「飛んではいけない」とか注意書きは見つかりますが見つけ方や諦めるしか無いのか等はっきりしたことが分からなかったので、 saves 内のデータから探してみることにしました。

以下のプログラムを実行して出来る entities.txt に id, pos, customname がリストアップされるので id=:allay を探し、近くまで移動してまた実行して移動先を探しを繰り返して、無事発見出来ました。

ワールドで記録されている全 entity が出るので、"何か" を探すのに役立つかもしれませんので公開しておきます。

```java:MinecraftEntities.java
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

import com.google.gson.*;
import com.google.gson.stream.JsonWriter;

public class MinecraftEntities {
  public static void main(String[] args) throws Exception {
    File world = new File("<world folder>");
    File entitiesFolder = new File(world, "entities");

    try(PrintStream out = new PrintStream(new FileOutputStream("entities.txt"))) {
      for(File mca : entitiesFolder.listFiles((File file) -> file.getName().endsWith(".mca") && file.length() > 0)) {
        StringWriter sw = new StringWriter();
        new MCAFile(mca).toJson(sw, null);
        JsonArray rootArray = new Gson().fromJson(sw.toString(), JsonArray.class);
        for(JsonElement e1 : rootArray) {
          JsonObject o1 = e1.getAsJsonObject();
          JsonArray entities = o1.getAsJsonArray("Entities");
          for(JsonElement e2 : entities) {
            JsonObject entity = e2.getAsJsonObject();
            String id = entity.getAsJsonPrimitive("id").getAsString();
            if(id.startsWith("minecraft:")) id = id.substring("minecraft".length());
            JsonArray a1 = entity.getAsJsonArray("Pos");
            int x = a1.get(0).getAsInt();
            int y = a1.get(1).getAsInt();
            int z = a1.get(2).getAsInt();
            String customName = entity.has("CustomName")
                ? entity.getAsJsonPrimitive("CustomName").getAsString()
                : null;
            out.println("id=" + id + ", X=" + x + ", Y=" + y + ", Z=" + z
                + (customName == null ? "" : ", customName=" + customName));
          }
        }
      }
    }
/*
    for(File mca : entitiesFolder.listFiles((File file) -> file.getName().endsWith(".mca") && file.length() > 0)) {
      String jsonfile = mca.getName() + ".json";
      try(Writer w = new BufferedWriter(new FileWriter(jsonfile))) {
        new MCAFile(mca).toJson(w, " ");
      }
    }
*/
  }
}

class MCAFile {
  private File file;

  MCAFile(File file) {
    if(!file.exists() || file.length() == 0) throw new IllegalArgumentException(file + " is empty");
    this.file = file;
  }

  void toJson(Writer writer, String indent) throws IOException, DataFormatException {
    try(RandomAccessFile raf = new RandomAccessFile(file, "r")) {
      SectorAllocationTable[] sats = SectorAllocationTable.readHeader(raf);

      try(Context co = new Context(writer, indent)) {
        co.beginArray();
        //データセクタ
        for(SectorAllocationTable sat : sats) {
          if(sat.isEmpty()) continue;

          byte[] entry = sat.read(raf);
          co.setIn(new ByteArrayInputStream(entry), sat.timestamp);
          while(co.available() > 0) parse(co);
        }
        co.endArray();
      }
    }
  }

  private static class SectorAllocationTable {
    static final int SIZE = 4096;

    static SectorAllocationTable[] readHeader(RandomAccessFile raf) throws IOException {
      byte[] buf = new byte[SIZE * 2];
      raf.seek(0);
      raf.readFully(buf);
      ByteBuffer bb = ByteBuffer.wrap(buf);

      SectorAllocationTable[] sectors = new SectorAllocationTable[SIZE / 4];
      for(int i=0, j=0; i<SIZE; i+=4, j++) {
        int value = bb.getInt(i);
        int offset = (value >> 8) & 0x00ffffff;
        int sectorCount = value & 0xff;
        int timestamp = bb.getInt(i + SIZE); //EPICH[s]
        sectors[j] = new SectorAllocationTable(offset, sectorCount, timestamp * 1000L);
      }
      return sectors;
    }

    private final int offset, count;
    final long timestamp;
    private SectorAllocationTable(int offset, int count, long timestamp) {
      this.offset = offset;
      this.count = count;
      this.timestamp = timestamp;
    };

    boolean isEmpty() { return offset == 0 || count == 0; }

    byte[] read(RandomAccessFile raf) throws IOException, DataFormatException {
      raf.seek(offset * SIZE);
      int length = raf.readInt();
      int compressionType = raf.readByte(); //1=GZip(RFC1952), 2=Zlib(RFC1950), 3=Uncompressed
      if(compressionType != 2) throw new IllegalStateException("compression-type: " + compressionType);

      byte[] data = new byte[length];
      if(raf.read(data) != length) throw new IllegalStateException("length error: request=" + length);

      //Zlib解凍
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buf = new byte[SIZE];
      Inflater decompresser = new Inflater();
      decompresser.setInput(data, 0, data.length);
      for(int size; (size=decompresser.inflate(buf)) > 0; ) baos.write(buf, 0, size);
      decompresser.end();
      return baos.toByteArray();
    }
  }

  private static class Context implements Closeable {
    private DataInputStream in;
    private JsonWriter out;

    Context(Writer out, String indent) {
      this.out = new JsonWriter(out);
      if(indent != null) this.out.setIndent(indent);
    }
    void setIn(InputStream in, long timestamp) { this.in = new DataInputStream(in); }

    int available() throws IOException { return in.available(); }
    byte readByte() throws IOException { return in.readByte(); }
    short readShort() throws IOException { return in.readShort(); }
    int readInt() throws IOException { return in.readInt(); }
    long readLong() throws IOException { return in.readLong(); }
    float readFloat() throws IOException { return in.readFloat(); }
    double readDouble() throws IOException { return in.readDouble(); }
    String readString() throws IOException {
      int length = in.readShort();
      return new String(in.readNBytes(length), 0, length, StandardCharsets.UTF_8);
    }

    void name(String name) throws IOException { out.name(name); }
    void value(long value) throws IOException { out.value(value); }
    void value(float value) throws IOException { out.value(value); }
    void value(double value) throws IOException { out.value(value); }
    void value(String value) throws IOException { out.value(value); }
    void beginArray() throws IOException { out.beginArray(); }
    void endArray() throws IOException { out.endArray(); }
    void beginObject() throws IOException { out.beginObject(); }
    void endObject() throws IOException { out.endObject(); }

    @Override
    public void close() throws IOException {
      if(out != null) out.close();
      out = null;
    }
  }

  private enum Translator {
    END {
      void translate(Context co) throws IOException { /*no process*/ }
    },
    BYTE {
      void translate(Context co) throws IOException { co.value(co.readByte()); }
    },
    SHORT {
      void translate(Context co) throws IOException { co.value(co.readShort()); }
    },
    INT {
      void translate(Context co) throws IOException { co.value(co.readInt()); }
    },
    LONG {
      void translate(Context co) throws IOException { co.value(co.readLong()); }
    },
    FLOAT {
      void translate(Context co) throws IOException { co.value(co.readFloat()); }
    },
    DOUBLE {
      void translate(Context co) throws IOException { co.value(co.readDouble()); }
    },
    BYTE_ARRAY {
      void translate(Context co) throws IOException { transArray(co, BYTE); }
    },
    STRING {
      void translate(Context co) throws IOException { co.value(co.readString()); }
    },
    LIST {
      void translate(Context co) throws IOException {
        co.beginArray();
        Translator tr = Translator.get(co.readByte());
        for(int length=co.readInt(), i=0; i<length; i++) tr.translate(co);
        co.endArray();
      }
    },
    COMPOUND {
      void translate(Context co) throws IOException {
        co.beginObject();
        while(parse(co) != Translator.END);
        co.endObject();
      }
    },
    INT_ARRAY {
      void translate(Context co) throws IOException { transArray(co, INT); }
    };

    static Translator get(int tagtype) {
      return values()[tagtype];
    }

    abstract void translate(Context co) throws IOException;

    private static void transArray(Context co, Translator tr) throws IOException {
      co.beginArray();
      for(int length=co.readInt(), i=0; i<length; i++) tr.translate(co);
      co.endArray();
    }
  }

  private static Translator parse(Context co) throws IOException {
    Translator tr = Translator.get(co.readByte());
    if(tr != Translator.END) {
      String name = co.readString();
      if(!name.isEmpty()) co.name(name);
    }
    tr.translate(co);
    return tr;
  }
}
```