Предизвикателство · обратно инженерство · шестнайсетичен редактор

Непознатият формат

Получаваш три файла .s7m с 3D модели. Документация няма. Задачата е да разбереш как са устроени само от байтовете, да напишеш програма, която ги чете, и накрая да видиш моделите на екрана.

ФайловетеS_Well_01.s7m (31 KB), W_Windmill_01.s7m (196 KB), N_Tavern_01.s7m (533 KB).
⬇ Свали моделите (zip)
ИнструментиImHex или HxD, калкулатор, лист хартия и твоят език за програмиране (C# с BinaryReader е чудесен избор).
Главното правилоВсяка хипотеза се проверява. „Мисля, че това е броят на върховете“ става „знам“ едва когато числата се сходят.

Правила

Помощни функции

Инструменти, които ще ти трябват по пътя. Не казват нищо за формата, само как се четат различни видове данни с BinaryReader.

Покажи кода (C#)
using System.Text;

static class Bin
{
    // 16-битово реално число (half) → float, ръчно, с битови операции.
    // Структура: 1 бит знак | 5 бита степен (изместване 15) | 10 бита мантиса
    public static float HalfToFloat(ushort h)
    {
        int sign     = (h >> 15) & 0x1;
        int exponent = (h >> 10) & 0x1F;
        int mantissa =  h        & 0x3FF;
        int bits;

        if (exponent == 0)
        {
            if (mantissa == 0)
                bits = sign << 31;                          // ±0
            else
            {
                // денормализирано число: изместваме, докато се появи скритата единица
                exponent = 1;
                while ((mantissa & 0x400) == 0) { mantissa <<= 1; exponent--; }
                mantissa &= 0x3FF;
                bits = (sign << 31) | ((exponent + 127 - 15) << 23) | (mantissa << 13);
            }
        }
        else if (exponent == 31)
            bits = (sign << 31) | 0x7F800000 | (mantissa << 13);   // ±∞ или NaN
        else
            bits = (sign << 31) | ((exponent + 127 - 15) << 23) | (mantissa << 13);

        return BitConverter.Int32BitsToSingle(bits);
    }

    public static float ReadHalf(BinaryReader r) => HalfToFloat(r.ReadUInt16());
    // В .NET 5+ има и готово: (float)r.ReadHalf(). Сравни двата резултата!

    // n байта като текст, напр. подпис от 4 знака
    public static string ReadFixedString(BinaryReader r, int n)
        => Encoding.ASCII.GetString(r.ReadBytes(n));

    // Текст, завършващ с нулев байт, на дадена позиция.
    // След четенето позицията във файла се връща там, където е била.
    public static string ReadCStringAt(BinaryReader r, long pos)
    {
        long back = r.BaseStream.Position;
        r.BaseStream.Position = pos;
        var bytes = new List<byte>();
        byte b;
        while ((b = r.ReadByte()) != 0) bytes.Add(b);
        r.BaseStream.Position = back;
        return Encoding.Latin1.GetString(bytes.ToArray());
    }

    // Прескача до следващия адрес, кратен на n (ако вече не е)
    public static void Align(BinaryReader r, int n)
    {
        long p = r.BaseStream.Position;
        r.BaseStream.Position = p + (n - p % n) % n;
    }
}

// Използване:
// using var r = new BinaryReader(File.OpenRead("S_Well_01.s7m"));
// string podpis = Bin.ReadFixedString(r, 4);
// uint   chislo = r.ReadUInt32();     // little-endian, както във файла

Упражнение: провери HalfToFloat за всички 65 536 възможни стойности срещу вградения (float)BitConverter.UInt16BitsToHalf(h). Колко от тях са NaN?

Етапи

1

Разузнаване

Отвори най-малкия файл. Не пиши код, само гледай.

Подсказка

Сложи трите файла един до друг. Сравнявай байтовете в началото: кои са еднакви (подпис, версия), кои са различни и растат заедно с размера на файла (брой елементи).

2

Заглавната част

Първите десетки байтове описват целия файл. Разбери значението на колкото може повече полета.

Подсказка

Байтовете са little-endian: 79 03 00 00 е 0x379 = 889. Реалното число 1.0 като 4-байтов float е 00 00 80 3F, а отрицателните реални числа завършват на байт ≥ 80.

3

Блоковете

След заглавната част файлът е поредица от блокове, всеки със собствена малка заглавна част. Открий нейната структура.

Напиши програма, която обхожда файла и отпечатва списък на блоковете: отместване, тип, размер.

Готово, когато: за всеки от трите файла последният блок свършва точно в края на файла.

Подсказка

Текстовете от етап 1 също са блок. Потърси числото, равно на дължината на текстовата част, точно преди нея.

4

Върховете

Един от блоковете съдържа данни за всеки връх. Колко байта е един връх? Какво има в тях?

Готово, когато: минималните и максималните x, y, z на всички върхове съвпадат (приблизително) с шестте числа от етап 2, дължината на всяка нормала е ≈ 1, а UV координатите са предимно между 0 и 1.

Подсказка

В .NET има тип Half: BitConverter.ToHalf. Байтът 127 или 128 в опакована нормала означава приблизително 0.

5

Стени и материали

Готово, когато: сборът на индексите по частите е равен на брояча от заглавната част, а нито един индекс не е ≥ броя на върховете.

6

Покажи го

Програмата отпечатва отчет в конзолата и записва модела във формат .obj:

Файл:      S_Well_01.s7m
Версия:    ...
Върхове:   ...        Триъгълници: ...
Граници:   (...) – (...)
Блокове:   ...
Части:
  [0] ...  индекси ...–...  текстура ...
  ...
Записан:   S_Well_01.obj

Отвори .obj файла в Blender или в онлайн 3D преглед. Ако видиш кладенец, вятърна мелница и кръчма, формата е разшифрован.

★ 7

Тайните на мелницата

Мелницата и кръчмата имат блокове, които кладенецът няма. Един от тях съдържа имена. Друг е масив с по 8 байта за всеки връх.

★ 8

Спецификация

Напиши документацията, която липсваше: таблица с всяко поле (отместване, размер, тип, значение) и отделно поле за това, което не си разбрала. Добрата спецификация признава какво остава неизвестно.