Проще всего это сделать в XML. Тогда мы легко можем добавлять новые теги и игнорировать то, что в XML не входит.
Двоичный файл надо делать на манер XML — в виде иерархической конструкции из коротеньких потоков. Самое сложное — чтобы по этому двоичному файлу не надо было бегать туда-сюда, как по гоночному треку. Для этого на каждом из уровней иерархии бывает одно из двух.
1. Запросить некую последовательность кодов, а затем спрашивать: есть этот код? А есть этот? Что-то типа (для простоты пишу как на C++)…
BlockReader blk;
int order1[] = { opHeader, opSettings, dirData };
BlockOrder order(blk, order1);
order.require(opHeader);
// считать заголовок
author = blk.readString();
if (order.get(opSettings) {
// считать настройки
}
order.require(dirData);
blk.enterDir();
// считать данные таким же образом — там может быть свой BlockOrder
blk.leaveDir();
Да, для чего мы запрашиваем порядок блоков дважды?
Первый раз — вот для чего. Представим себе устаревший файл, в котором нет блока настроек. Мы считываем заголовок, видим вместо блока настроек каталог с данными, и сразу вопрос: тут что-то пропущено или что-то лишнее?
Второй раз — для наглядности (код комментирует сам себя) и защиты от ошибок, когда заявленный в начале порядок не совпадает с реальным.
2. Просто считываем блоки по одному и интерпретируем. Обычно это бывает во всяких коллекциях.
while (blk.getBlock()) {
switch (blk.opcode) {
case dirTiledLevel:
// считать плиточный уровень
case dirGraphicLevel:
// считать уровень с фоном — цельной картинкой
}
}
Запрещается смешивать упорядоченное с коллекциями.
Можно также в заголовке каждого блока сделать бит: Essential. Старая версия, наткнувшись на такой блок и не считав ни байта (или наткнувшись на каталог и не войдя), выводит ошибку: версия явно новее, считать невозможно. Это бывает важно, когда в файле есть перекрёстные ссылки.
Для записи подобных файлов приходится накапливать блок в памяти, а затем сваливать его в один присест (ведь надо записать длину блока). Впрочем, есть и версия для структур, которые заранее знают свою длину — тогда накапливать в памяти излишне.
Далее. В заголовке можно сохранить поля «SavedWithVersion», «MinRequiredVersion», «MinFullySupportingVersion».
Добавляем новый блок (или новое поле в имеющийся блок) — поднимаем MinFullySupportingVersion до текущей. Изменяем структуру так, что ломаем совместимость — поднимаем MinRequiredVersion.
То, что версия слегка устарела, можно ловить и по косвенным признакам — какой-то блок считан не полностью, в какой-то каталог не вошли. У блока/каталога может быть флаг Compatibility — их проверять не надо. И наоборот — если важный блок отмечен как Compatibility, тоже версия устарела. Ясное дело, флаги Essential и Compatibility не могут попадаться вместе.
Как это делать, если формат сохранения — база данных (например, SQLite), я не в курсе.
А вот сохранение в устаревший формат, когда структуры данных ушли далеко вперёд, придётся налаживать руками. Можно ли в принципе, как преобразовать в старый формат, и т.д.