klensy / wt-tools

War Thunder resource extraction tools

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Not an issue, but a question

Keksilton opened this issue · comments

Recently I have found an interesting project called Kaitai, they are collaborating with construct developers to enable import of python struct code to their format, but it's not available yet.

I have been trying to port it myself, so the formats would not be tied to python, but I have run into some issues trying to understand the format.

not_packed_stream = Struct(
        "data_start_offset" / Tell,
        "filename_table_offset" / Int32ul,
        "files_count" / Int32ul,
        Seek(8, 1),
        "filedata_table_offset" / Int32ul,
        "filename_table" / filename_table,
        "file_data_table" / file_data_table,
)

Here is what the file looks like:
image

I have to read the values after the file has been parsed and discovered that :
data_start_offset is equal to 0
filename_table_offset is equal to 32
files_count is equal to 23
filedata_table_offsed is equal to 672

I don't understand why Tell is returning 0 in this case, since the pointer would have moved, after reading the header.
I also don't understand where the 32 is coming from.
I'm assuming the 00 00 00 is the tail from zstd_stream, then there is 17 00 which translated to 23, but I would expect a 4 byte integer, not just 2 bytes
After this 23, there is a0 02 it translates to 672, which also fits the number I need, but it's 2 bytes wide again.
It also contradicts the Seek(8,1) line
I have tried searching entire file for a a 32 bit, unsigned, little-endian integer equal to 672 and found nothing.

The section after after_obfs does not seem to contain any of those numbers.
image

I have also tried to look for a "visualizer" for python's construct, to better understand the relation between the structure description and file contents location, but was unable to find any.

Could you please explain at least some of those issues?
I would really appreciate it.

I could translate this to Russian if it's necessary.

commented

You were looking at an obfuscated compressed stream.
To get to the unpacked stream, you had to do a few simple transformations of it:
not_packed_bytes = unzstd(deobfuscate(file_bytes))

Yesterday I was dealing with files unpacking and made an auxiliary tool.
partial_constructor.py
For clarity, I implemented the unpacking adapter as a composition of adapters:
ZstdPackedCon = NotPackedStreamAdapter(ZstdStreamAdapter(DeobfsAdapter(Bytes(packed_size))))

Next, the first 128 bytes from the trace files at the decompression stages.

char.vromfs.bin

00000000  56 52 46 78 00 00 50 43  50 17 09 00 bd eb 08 c0  |VRFx..PCP.......|
00000010  08 00 00 00 97 00 07 02  7d 1f 7a 57 af a0 18 f9  |........}.zW....|
00000020  55 4e 49 a5 d4 ca 55 32  00 00 00 17 00 a0 02 e0  |UNI...U2........|
00000030  f6 06 01 1e 35 47 5f 72  8a a3 b4 c7 e0 f3 09 02  |....5G_r........|
00000040  19 29 3a 4e 61 76 88 90  63 6f 6e 66 69 67 2f 61  |.):Nav..config/a|
00000050  74 74 61 63 68 61 62 6c  65 2e 62 6c 6b 62 6f 74  |ttachable.blkbot|
00000060  73 63 6c 61 6e 5f 72 65  77 61 72 64 72 65 77 5f  |sclan_rewardrew_|
00000070  73 6b 69 6c 6c 64 65 63  61 66 61 75 6c 74 5f 75  |skilldecafault_u|
00000080

deobfuscate: data.zstd

00000000  28 b5 2f fd a0 50 17 09  00 e4 1c 0f 9c d8 1d 20  |(./..P......... |
00000010  00 00 00 17 00 a0 02 e0  f6 06 01 1e 35 47 5f 72  |............5G_r|
00000020  8a a3 b4 c7 e0 f3 09 02  19 29 3a 4e 61 76 88 90  |.........):Nav..|
00000030  63 6f 6e 66 69 67 2f 61  74 74 61 63 68 61 62 6c  |config/attachabl|
00000040  65 2e 62 6c 6b 62 6f 74  73 63 6c 61 6e 5f 72 65  |e.blkbotsclan_re|
00000050  77 61 72 64 72 65 77 5f  73 6b 69 6c 6c 64 65 63  |wardrew_skilldec|
00000060  61 66 61 75 6c 74 5f 75  73 65 72 79 6e 61 6d 69  |afault_userynami|
00000070  63 66 6c 69 67 68 74 6d  6f 64 65 67 61 6d 65 5f  |cflightmodegame_|
00000080

unzstd: data

00000000  20 00 00 00 17 00 00 00  00 00 00 00 00 00 00 00  | ...............|
00000010  a0 02 00 00 17 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  e0 00 00 00 00 00 00 00  f6 00 00 00 00 00 00 00  |................|
00000030  06 01 00 00 00 00 00 00  1e 01 00 00 00 00 00 00  |................|
00000040  35 01 00 00 00 00 00 00  47 01 00 00 00 00 00 00  |5.......G.......|
00000050  5f 01 00 00 00 00 00 00  72 01 00 00 00 00 00 00  |_.......r.......|
00000060  8a 01 00 00 00 00 00 00  a3 01 00 00 00 00 00 00  |................|
00000070  b4 01 00 00 00 00 00 00  c7 01 00 00 00 00 00 00  |................|
00000080

I see, that makes sense. Thank you!
I guess I will have to implement those transformations outside of Kaitai and just let it use it.

@kotiq Could you help me a bit more?

  1. I have tried to read though the code in your blk project, but it's hard for me to understand it, since I'm not a python developer.
    I'm a bit confused by the first few bytes of the nm file, since those are not regular characters. Am I missing something, or should I just ignore it?
    image

  2. I'm also having trouble understanding blk format. I tried to follow this post, but it wasn't enough for me https://www.reddit.com/r/Warthunder/comments/o5r4v1/heads_up_on_vrom_and_blk_format_changes_for_data/
    image

Once it works - I want to publish C# implementation on Github as well.

I speak Russian as well
Feel free to contact me via discord Keksilton#6024

commented

Используется фреймворк Construct, сам python прост.
Двоичные файлы нового формата со стороны распаковки я разделил на два класса:
Толстые: автономные файлы, которым достаточно для распаковки самих себя. Примеры в game.vromfs.bin. Каждый такой файл несет в себе таблицу строк.
Тонкие: таблица строк для них общая для каждой FS. Примеры в aces.vromfs.bin.
Таблица строк хранится в файле nm. Из таблицы строк собираются имена параметров и секций и значения для строковых параметров.

1

В таблице строк подряд идет пара чисел в ULEB128 число строк в таблице и размер блока строк в байтах.

2

Тонкий файл начинается с 0 байта.
Далее в ULEB128 идут количество всех секций, включая корневую секцию, и количество параметров из всех секций.
Потом размер блока параметров, каждый из которых в закодированном виде превышают 4 байта, в байтах в ULEB128 и блок параметров. В нем "длинные" значения записаны подряд в порядке упоминания в таблице описания параметров, а навигация осуществляется смещением в байтах от начала блока. Для более 2 длинных значений:

[первое длинное значение]
[второе длинное значение]
...
[последнее длинное значение]

Длинные значения в случае тонкого файла у последовательностей из 2- и 3- целых и 2-, 3-, 4-, 12- рациональных и у длинного целого как 8 байт LE.
И следом таблица описания параметров и таблица описания секций.
Таблица описания параметров состоит из 8-байтовых записей, по одной на параметр.
Каждая такая запись состоит из 3-байтового индекса LE в таблице строк, описывающего имя, байта, описывающего тип значения и 4 байтов, описывающих значение.
В случае длинных значений они содержат LE индекс. Для строк у числа отбрасывается старший бит.
Для строк значение берется по номеру строки из таблицы строк. Для остальных - значение считывается из блока параметров, индекс представляет смещение относительно начала этого блока. Считывается число байт, согласно типу значения параметра: каждое целое или рациональное занимает 4 байта, последовательности хранятся как блоки целых LE или чисел с плавающей точкой одинарной точности LE подряд.
В случае коротких значений, байты кодируют сами значения: целое LE, число с плавающей точкой одинарной точности LE, четырехкомпонентный цвет как BGRA или булево значение как целое LE: 0 - ложь, 1 - истина.
Таблица описания секций состоит из блоков переменной длины, по одному на секцию, включая корневую секцию.
Каждый блок содержит описатель имени секции в ULEB128. Для корневой секции в этом месте 0, поэтому, все остальные описатели на 1 больше номера строки по порядку в таблице строк. Для получения индекса вычитаем из описателя единицу, а корневая секция безымянная.
Далее следует число параметров в секции, и число подсекций, оба в ULEB128.
Если секция содержит подсекции, то следующее число в ULEB128 - индекс первого блока, описывающего секцию в таблице описания секций.
Секция заполняется так.
По числу параметров в ней берутся параметры из таблицы описания параметров подряд. Каждый параметр строится по описывающему блоку. Например, для числа секций больше 2, где каждый параметр строится по строке из таблицы:

Индекс Содержимое
-----------------------------
0      [параметры корневой секции]
1      [параметры 1-й секции уровня 1]
...
X      [параметры последней секции уровня 1]
X + 1  [параметры 2-й секции уровня 2]
...
Y      [параметры последней секции уровня 2]
...
Z      [параметры первой секции последнего уровня]
...
W      [параметры последней секции последнего уровня]

По числу подсекций берутся подсекции из таблицы описания подсекций подряд, начиная с указанного индекса первого блока. Аналогично параметрам, для нетривиальной корневой секции в соответствии с обходом секции в ширину:

Индекс Содержимое
-----------------------------
0      [корневая секция]
1      [1-я секция уровня 1]
...
K      [последняя секция уровня 1]
K + 1  [1-я секция уровня 2]
...
L      [последняя секция уровня 2]
...
M      [1-я секция последнего уровня]
...
N      [последняя секция последнего уровня]
commented

В качестве иллюстрации я упакую простую секцию как тонкую и распакую ее обратно вручную.
Пусть после вычислений над секцией она имеет вид:

vec4f:p4=1.25, 2.5, 5.0, 10.0
int:i=42
long:i64=0x40

alpha{
  str:t="hello"
  bool:b=yes
  color:c=1, 2, 3, 4

  gamma{
    vec2i:ip2=3, 4
    vec2f:p2=1.25, 2.5
    transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
  }
}

beta{
  float:r=1.25
  vec2i:ip2=1, 2
  vec3f:p3=1.25, 2.5, 5.0
}

Ей будет соответствовать множество двоичных представлений, которые различаются из-за различных обходов секции при
построении таблицы строк.
Пусть таблица строк имеет представление:

00000000  0f 57 76 65 63 34 66 00  69 6e 74 00 6c 6f 6e 67  |.Wvec4f.int.long|
00000010  00 61 6c 70 68 61 00 73  74 72 00 62 6f 6f 6c 00  |.alpha.str.bool.|
00000020  63 6f 6c 6f 72 00 67 61  6d 6d 61 00 76 65 63 32  |color.gamma.vec2|
00000030  69 00 76 65 63 32 66 00  74 72 61 6e 73 66 6f 72  |i.vec2f.transfor|
00000040  6d 00 62 65 74 61 00 66  6c 6f 61 74 00 76 65 63  |m.beta.float.vec|
00000050  33 66 00 68 65 6c 6c 6f  00                       |3f.hello.|
00000059

И сама секция имеет представление:

00000000  00 04 0c 6c 00 00 a0 3f  00 00 20 40 00 00 a0 40  |...l...?.. @...@|
00000010  00 00 20 41 40 00 00 00  00 00 00 00 01 00 00 00  |.. A@...........|
00000020  02 00 00 00 00 00 a0 3f  00 00 20 40 00 00 a0 40  |.......?.. @...@|
00000030  03 00 00 00 04 00 00 00  00 00 a0 3f 00 00 20 40  |...........?.. @|
00000040  00 00 80 3f 00 00 00 00  00 00 00 00 00 00 00 00  |...?............|
00000050  00 00 80 3f 00 00 00 00  00 00 00 00 00 00 00 00  |...?............|
00000060  00 00 80 3f 00 00 a0 3f  00 00 20 40 00 00 a0 40  |...?...?.. @...@|
00000070  00 00 00 06 00 00 00 00  01 00 00 02 2a 00 00 00  |............*...|
00000080  02 00 00 0c 10 00 00 00  04 00 00 01 0e 00 00 80  |................|
00000090  05 00 00 09 01 00 00 00  06 00 00 0a 03 02 01 04  |................|
000000a0  0c 00 00 03 00 00 a0 3f  08 00 00 07 18 00 00 00  |.......?........|
000000b0  0d 00 00 05 20 00 00 00  08 00 00 07 2c 00 00 00  |.... .......,...|
000000c0  09 00 00 04 34 00 00 00  0a 00 00 0b 3c 00 00 00  |....4.......<...|
000000d0  00 03 02 01 04 03 01 03  0c 03 00 08 03 00        |..............|
000000de

Таблица строк.

hex:0
0F - names_count = 15 имен.
57 - Они занимают names_len = 87 байт.

-------------------------------------------
hex:2                str        pn_id sn_id
-------------------------------------------
766563346600         vec4f      0      1
696E7400             int        1      2
6C6F6E6700           long       2      3
616C70686100         alpha      3      4
73747200             str        4      5
626F6F6C00           bool       5      6
636F6C6F7200         color      6      7
67616D6D6100         gamma      7      8
766563326900         vec2i      8      9
766563326600         vec2f      9     10
7472616E73666F726D00 transform 10     11
6265746100           beta      11     12
666C6F617400         float     12     13
766563336600         vec3f     13     14
68656C6C6F00         hello     14     15

Где pn_id - индекс строки в таблице параметров как имени или значения строкового параметра
    sn_id - индекс строки в таблице секций как имени
    hex:n - смещение первого значения в соответствующем файле в шестнадцатеричном формате

Секция.

hex:0
00 - Признак тонкого файла. Имен здесь нет. Или признак для размерности в терминaх blk_unpack.py 

hex:1
04 - blocks_count = 4 секции, вместе с корневой.
0C - params_count = 12 параметров во всех секциях.

Блок значений длинных параметров.

hex:3
6C - Блок длинных значений параметров занимает param_data_len = 108 байт.

---------------------------------------------------------
hex:4                       value              offset size
---------------------------------------------------------
0000A03F 00002040 0000A040 (1.25, 2.5, 5.0,    0     16
00002041                    10.0)              -      -
40000000 00000000          64                 16      8
01000000 02000000          (1, 2)             24      8
0000A03F 00002040 0000A040 (1.25, 2.5, 5.0)   32     12 
03000000 04000000          (3, 4)             44      8 
0000A03F 00002040          (1.25, 2.5)        52      8
0000803F 00000000 00000000 ((1.0, 0.0, 0.0),  60     48 
00000000 0000803F 00000000  (0.0, 1.0, 0.0),   -      -
00000000 00000000 0000803F  (0.0, 0.0, 1.0),   -      -
0000A03F 00002040 0000A040  (1.25, 2.5, 5.0))  -      -

Таблица описания параметров.

---------------------------------------------------
hex:70             pn_id blk_type value        p_id 
---------------------------------------------------
000000 06 00000000  0    p4       #0@16         0   
010000 02 2A000000  1    i        42            1
020000 0C 10000000  2    i64      #16@8         2
040000 01 0E000080  4    t        #14           3
050000 09 01000000  5    b        1             4
060000 0A 03020104  6    c        (1, 2, 3, 4)  5
0C0000 03 0000A03F 12    r        1.25          6
080000 07 18000000  8    ip2      #24@8         7
0D0000 05 20000000 13    p3       #32@12        8
080000 07 2C000000  8    ip2      #44@8         9
090000 04 34000000  9    p2       #52@8        10
0A0000 0B 3C000000 10    m        #60@48       11

Где #x@y - y байтов со смещения x от начала блока длинных параметров
    #z   - pn_id значения строкового параметра
    p_id - индекс записи параметра в таблице описания секций

Таблица описания секций.

----------------------------------------------
hex:d0      s_id   sn_id p_count s_count ss_id
----------------------------------------------
00 03 02 01 0       -    3       2       1
04 03 01 03 1       4    3       1       3
0C 03 00    2      12    3       0       -
08 03 00    3       8    3       0       -

Где ss_id - индекс первой подсекции в секции

Построение таблицы параметров из таблицы строк, блока значений длинных параметров и таблицы описания параметров.

--------------------------------------------------------------------------------------------
p_id name      blk_type value
--------------------------------------------------------------------------------------------
 0   vec4f     p4       (1.25, 2.5, 5.0, 10.0)
 1   int       i        42
 2   long      i64      64
 3   str       t        hello
 4   bool      b        1
 5   color     c        (1, 2, 3, 4)
 6   float     r        1.25
 7   vec2i     ip2      (1, 2)
 8   vec3f     p3       (1.25, 2.5, 5.0)
 9   vec2i     ip2      (3, 4)
10   vec3f     p2       (1.25, 2.5)
11   transform m        ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (1.25, 2.5, 5.0))  

Построение секций из таблицы строк, таблицы параметров и таблицы описания секций.

Буду обходить таблицу описания секций снизу вверх.

-----------------------------
s_id name  p_ids       ss_ids
-----------------------------
3    gamma (9, 10, 11) -
2    beta  (6, 7, 8)   -
1    alpha (3, 4, 5)   (3)
0    -     (0, 1, 2)   (1, 2)      

Подсекции

// s_id = 3
gamma {
  vec2i:ip2=3, 4
  vec2f:p2=1.25, 2.5
  transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
}
// s_id = 2
beta{
  float:r=1.25
  vec2i:ip2=1, 2
  vec3f:p3=1.25, 2.5, 5.0
}
// s_id = 1
alpha{
  str:t="hello"
  bool:b=yes
  color:c=1, 2, 3, 4

  // s_id = 3
  gamma{
    vec2i:ip2=3, 4
    vec2f:p2=1.25, 2.5
    transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
  }
}

Корневая секция

//s_id = 0
vec4f:p4=1.25, 2.5, 5.0, 10.0
int:i=42
long:i64=0x40

// s_id = 1
alpha{
  str:t="hello"
  bool:b=yes
  color:c=1, 2, 3, 4

  // s_id = 3
  gamma{
    vec2i:ip2=3, 4
    vec2f:p2=1.25, 2.5
    transform:m=[[1.0, 0.0, 0.0] [0.0, 1.0, 0.0] [0.0, 0.0, 1.0] [1.25, 2.5, 5.0]]
  }
}

// s_id = 2
beta{
  float:r=1.25
  vec2i:ip2=1, 2
  vec3f:p3=1.25, 2.5, 5.0
}