解析 osu! Java 中的二进制数据库

Parse the osu! binary database in Java

正如标题所说,我必须阅读一个二进制文件,其中写有规范:

https://osu.ppy.sh/wiki/Db_(file_format)#collection.db

它是由用C#编写的程序编写的。除了我不知道如何继续获取数据,以及如何将它们存储到 objects 中。

关于继续进行的任何想法或示例?非常感谢:)

所以!因为规格是: 第一:int -> 版本号 第二个:int -> 集合数

但是!所有整数都是无符号的和小端的。所以我这样做了!

public static void main(String[] args) {
    try {
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("src/collection/collection.db")));
        byte b1 = in.readByte();
        byte b2 = in.readByte();
        byte b3 = in.readByte();
        byte b4 = in.readByte();
        int s = 0;
        s = s | (b4 & 0xff);
        s = (s << 8);
        s = s | (b3 & 0xff);
        s = (s << 8);
        s = s | (b2 & 0xff);
        s = (s << 8);
        s = s | (b1 & 0xff);

        System.out.println("Version des collections : " + s);

        b1 = in.readByte();
        b2 = in.readByte();
        b3 = in.readByte();
        b4 = in.readByte();
        s = 0;
        s = s | (b4 & 0xff);
        s = (s << 8);
        s = s | (b3 & 0xff);
        s = (s << 8);
        s = s | (b2 & 0xff);
        s = (s << 8);
        s = s | (b1 & 0xff);

        System.out.println("Nombre de collections : " +s);

    } catch (Exception e) {
        System.out.print("mdr fail");
    }
}

我希望我也帮助了正在搜索的人。

根据规格:

import java.io.*;
import java.nio.*;
import java.util.*;

public class OsuReader
{
    public static void main(String[] args) throws IOException
    {
        String kind = args[0];
        OsuReader reader = new OsuReader(args[1]);
        if (kind.equals("collection"))
        {
            CollectionDB db = reader.readCollectionDB();
            System.out.printf("Version: %d\n", db.version);
            for (CollectionItem item : db.collections)
            {
                System.out.println();
                System.out.printf("Name: %s\n", item.name);
                for (String hash : item.md5Hashes)
                {
                    System.out.printf("  Hash: %s\n", hash);
                }
            }
        }
        else if (kind.equals("scores"))
        {
            ScoresDB db = reader.readScoresDB();
            System.out.printf("Version: %d", db.version);
            for (Beatmap beatmap : db.beatmaps)
            {
                System.out.println("---");
                System.out.printf("Beatmap hash: %s\n", beatmaps.hash);
                for (Score score : beatmaps.scores)
                {
                    System.out.println("  ---");
                    System.out.printf("  Mode: %s (%d)\n", score.mode.name(), score.mode.byteValue);
                    System.out.printf("  Version: %d\n", score.version);
                    System.out.printf("  Beatmap MD5: %s\n", score.beatmapMd5Hash);
                    System.out.printf("  Player name: %s\n", score.playerName);
                    System.out.printf("  Replay MD5: %s\n", score.replayMd5Hash);
                    System.out.printf("  Scores: %d / %d / %d / %d / %d / %d\n",
                            score.numberOf300s, score.numberOf100s, score.numberOf50s, score.numberOfGekis,
                            score.numberOfKatus, score.numberOfMisses);
                    System.out.printf("  Replay score: %d\n", score.replayScore);
                    System.out.printf("  Max combo: %d\n", score.maxCombo);
                    System.out.printf("  Perfect combo: %s\n", score.perfectCombo ? "Yes" : "No");
                    System.out.printf("  Mods used: %s\n", score.modsUsed);
                    System.out.printf("  Timestamp: %s\n", score.timestamp);
                }
            }
        }
    }

    private DataInputStream reader;

    public OsuReader(String filename) throws IOException
    {
        this(new FileInputStream(filename));
    }

    public OsuReader(InputStream source)
    {
        this.reader = new DataInputStream(source);
    }

    // --- Primitive values ---

    public byte readByte() throws IOException
    {
        // 1 byte
        return this.reader.readByte();
    }

    public short readShort() throws IOException
    {
        // 2 bytes, little endian
        byte[] bytes = new byte[2];
        this.reader.readFully(bytes);
        ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        return bb.getShort();
    }

    public int readInt() throws IOException
    {
        // 4 bytes, little endian
        byte[] bytes = new byte[4];
        this.reader.readFully(bytes);
        ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        return bb.getInt();
    }

    public long readLong() throws IOException
    {
        // 8 bytes, little endian
        byte[] bytes = new byte[8];
        this.reader.readFully(bytes);
        ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        return bb.getLong();
    }

    public int readULEB128() throws IOException
    {
        // variable bytes, little endian
        // MSB says if there will be more bytes. If cleared,
        // that byte is the last.
        int value = 0;
        for (int shift = 0; shift < 32; shift += 7)
        {
            byte b = this.reader.readByte();
            value |= ((int) b & 0x7F) << shift;

            if (b >= 0) return value; // MSB is zero. End of value.
        }
        throw new IOException("ULEB128 too large");
    }

    public float readSingle() throws IOException
    {
        // 4 bytes, little endian
        byte[] bytes = new byte[4];
        this.reader.readFully(bytes);
        ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        return bb.getFloat();
    }

    public double readDouble() throws IOException
    {
        // 8 bytes little endian
        byte[] bytes = new byte[8];
        this.reader.readFully(bytes);
        ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        return bb.getDouble();
    }

    public boolean readBoolean() throws IOException
    {
        // 1 byte, zero = false, non-zero = true
        return this.reader.readBoolean();
    }

    public String readString() throws IOException
    {
        // variable length
        // 00 = empty string
        // 0B <length> <char>* = normal string
        // <length> is encoded as an LEB, and is the byte length of the rest.
        // <char>* is encoded as UTF8, and is the string content.
        byte kind = this.reader.readByte();
        if (kind == 0) return "";
        if (kind != 11)
        {
            throw new IOException(String.format("String format error: Expected 0x0B or 0x00, found 0x%02X", (int) kind & 0xFF));
        }
        int length = readULEB128();
        if (length == 0) return "";
        byte[] utf8bytes = new byte[length];
        this.reader.readFully(utf8bytes);
        return new String(utf8bytes, "UTF-8");
    }

    public Date readDate() throws IOException
    {
        long ticks = readLong();
        long TICKS_AT_EPOCH = 621355968000000000L;
        long TICKS_PER_MILLISECOND = 10000;

        return new Date((ticks - TICKS_AT_EPOCH)/TICKS_PER_MILLISECOND);
    }

    // --- Composite structures ---

    public CollectionDB readCollectionDB() throws IOException
    {
        CollectionDB result = new CollectionDB();
        result.version = readInt();
        int count = readInt();
        result.collections = new ArrayList<CollectionItem>(count);
        for (int i = 0; i < count; i++)
        {
            CollectionItem item = readCollectionItem();
            result.collections.add(item);
        }
        return result;
    }

    public CollectionItem readCollectionItem() throws IOException
    {
        CollectionItem item = new CollectionItem();
        item.name = readString();
        int count = readInt();
        item.md5Hashes = new ArrayList<String>(count);
        for (int i = 0; i < count; i++)
        {
            String md5Hash = readString();
            item.md5Hashes.add(md5Hash);
        }
        return item;
    }

    public ScoresDB readScoresDB() throws IOException
    {
        ScoresDB result = new ScoresDB();
        result.version = readInt();
        int count = readInt();
        result.beatmaps = new ArrayList<Beatmap>(count);
        for (int i = 0; i < count; i++)
        {
            Beatmap beatmap = readBeatmap();
            result.beatmaps.add(beatmap);
        }
        return result;
    }

    public Beatmap readBeatmap() throws IOException
    {
        Beatmap result = new Beatmap();
        result.md5Hash = readString();
        int count = readInt();
        result.scores = new ArrayList<Score>();
        for (int i = 0; i < count; i++)
        {
            Score score = readScore();
            result.scores.add(score);
        }
        return result;
    }

    public Score readScore() throws IOException
    {
        Score result = new Score();
        result.mode = GameplayMode.valueOf(readByte());
        result.version = readInt();
        result.beatmapMd5Hash = readString();
        result.playerName = readString();
        result.replayMd5Hash = readString();
        result.numberOf300s = readShort();
        result.numberOf100s = readShort();
        result.numberOf50s = readShort();
        result.numberOfGekis = readShort();
        result.numberOfKatus = readShort();
        result.numberOfMisses = readShort();
        result.replayScore = readInt();
        result.maxCombo = readShort();
        result.perfectCombo = readBoolean();
        result.modsUsed = OsuMod.valueOf(readInt());
        result.unknown1 = readString();
        result.timestamp = readDate();
        result.unknown2 = readInt();
        result.unknown3 = readInt();
        result.unknown4 = readInt();
        return result;
    }

    public class CollectionDB
    {
        public int version; // 20150203
        public List<CollectionItem> collections;
    }

    public class CollectionItem
    {
        public String name;
        public List<String> md5Hashes;
    }

    public class ScoresDB
    {
        public int version; // 20150204
        public List<Beatmap> beatmaps;
    }

    public class Beatmap
    {
        public String md5Hash;
        public List<Score> scores;
    }

    public enum GameplayMode
    {
        OsuStandard((byte) 0),
        Taiko((byte) 1),
        CTB((byte) 2),
        Mania((byte) 3);

        public final byte byteValue;

        private GameplayMode(byte byteValue)
        {
            this.byteValue = byteValue;
        }

        public static GameplayMode valueOf(byte byteValue)
        {
            for (GameplayMode item : values())
            {
                if (item.byteValue == byteValue) return item;
            }
            throw new IllegalArgumentException("byteValue");
        }
    }

    public enum OsuMod
    {
        NoFail(1),
        Easy(2),
        NoVideo(4),
        Hidden(8),
        HardRock(16),
        SuddenDeath(32),
        DoubleTime(64),
        Relax(128),
        HalfTime(256),
        Nightcore(512),
        Flashlight(1024),
        Autoplay(2048),
        SpunOut(4096),
        Relax2(8192),
        Perfect(16384),
        Key4(32768),
        Key5(65536),
        Key6(131072),
        Key7(262144),
        Key8(524288),
        keyMod(1015808),
        FadeIn(1048576),
        Random(2097152),
        LastMod(4194304);

        public final int bit;

        private OsuMod(int bit)
        {
            this.bit = bit;
        }

        public static EnumSet<OsuMod> valueOf(int bits)
        {
            EnumSet<OsuMod> result = EnumSet.noneOf(OsuMod.class);
            for (OsuMod flag : OsuMod.values())
            {
                if ((bits & flag.bit) == flag.bit)
                {
                    result.add(flag);
                }
            }
            return result;
        }
    }

    public class Score
    {
        public GameplayMode mode;
        public int version; // 20150203
        public String beatmapMd5Hash;
        public String playerName;
        public String replayMd5Hash;
        public short numberOf300s;
        public short numberOf100s;
        public short numberOf50s;
        public short numberOfGekis;
        public short numberOfKatus;
        public short numberOfMisses;
        public int replayScore;
        public short maxCombo;
        public boolean perfectCombo;
        public EnumSet<OsuMod> modsUsed;
        public String unknown1;
        public Date timestamp;
        public int unknown2;
        public int unknown3;
        public int unknown4;
    }
}