Ayuda para parsear este formato...

Iniciado por Eleкtro, 10 Diciembre 2016, 11:56 AM

0 Miembros y 1 Visitante están viendo este tema.

Eleкtro

Buenas!

Primero me gustaría aclarar que el propósito de esto es ético, y cualquier persona dispuesta a ayudarme a aclararme las ideas con esta duda que tengo, podría verse recompensada en un futuro próximo, ya que un grupo muy importante (y veterano) de Rom-Hacking está llevando a cabo la traducción al Español del videojuego Persona 3 para PS2, y yo he decidido prestarles ayuda en una pequeña cosa que necesitan, pero me encuentro atascado...

Este es el archivo con el que estoy trabajando:

El archivo se llama datBootsHelp.bmd y el formato del archivo, BMD, es un formato privativo el cual contiene 2 tablas,
necesito parsear una de esas tablas para traducir de forma programática los textos que contiene, y por textos me refiero a estas descripciones en Inglés:



En la siguiente imagen os muestro las especificaciones conocidas del formato BMD, y los valores que he obtenido analizando el archivo de ejemplo datBootsHelp.bmd el cual he compartido al principio del post.



Las filas marcadas con un signo verde son las que he conseguido localizar y parsear de forma programática sin problemas, las filas con un aspa roja son las que todavía no, y la fila con un signo de interrogación es el offset con el que tengo problemas y por ende me impide localizar los valores del aspa roja...

El problema que tengo es que no consigo determinar en que offset se delimita el final de la primera tabla, con el inicio de la segunda tabla, ni tampoco entiendo cual es la longitud exacta (contando los bytes ceros/nulos, se entiende) de los campos/textos de la segunda tabla, es decir, no consigo entender donde empieza el identificador de texto, cual es su longitud/capacidad, y donde empieza la descripción de texto y cual es su longitud/capacidad. Si analizo y comparo cada campo por individual para intentar averiguar la longitud, me da diferentes longitudes así que no me cuadra nada... pero eso solo significa que lo estoy analizando mal, por que deben tener una longitud máxima el identificador la descripción, vaya xD.

Supuestamente y a mi entender, la primera tabla termina en el offset 814 (0x32C) como se menciona en la imagen de arriba; en esta imagen de aquí abajo hago una selección empezando por ese offset hasta encontrar el inicio del primer identificador de texto ...y por medio hay 2 bytes (A4 1D) que no se lo que son:






Por último, comparto el código fuente que he desarrollado en el lenguaje VB.NET para leer los datos de la cabecera del formato y  la primera tabla:

Código (vbnet) [Seleccionar]
''' ----------------------------------------------------------------------------------------------------
''' <summary>
''' Represents the BMD File Format.
''' </summary>
''' ----------------------------------------------------------------------------------------------------
''' <remarks>
''' <see href="http://datacrystal.romhacking.net/wiki/Persona_3_and_4/BMD_%28File_Format%29"/>
''' </remarks>
''' ----------------------------------------------------------------------------------------------------
Public NotInheritable Class BMDFormat

#Region " Private Fields "

   ''' <summary>
   ''' Specifies the offset where the header begins.
   ''' </summary>
   Private ReadOnly HeaderOffset As Integer = &H0

   ''' <summary>
   ''' Specifies the offset where the non-text table begins. This is the first table in the BMD file format.
   ''' </summary>
   Private ReadOnly TableNonTextOffset As Integer = &H24

   ''' <summary>
   ''' Specifies the offset where the text table begins. This is the second table in the BMD file format.
   ''' </summary>
   Private TableTextOffset As Integer ' The value is determined later, at the end of the TableNonTextOffset table.

   ''' <summary>
   ''' Contains the offset location and length of the BMD header offsets.
   ''' </summary>
   Private ReadOnly HeaderOffsets As New Dictionary(Of String, KeyValuePair(Of Integer, Integer))(StringComparer.Ordinal) From {
       {"ChunkID", New KeyValuePair(Of Integer, Integer)(&H0, Nothing)},
       {"Size", New KeyValuePair(Of Integer, Integer)(&H4, &H5)},
       {"MagicIdentifier", New KeyValuePair(Of Integer, Integer)(&H8, &HB)},
       {"EndOfTextTable", New KeyValuePair(Of Integer, Integer)(&H10, &H11)},
       {"OffsetsAmount", New KeyValuePair(Of Integer, Integer)(&H14, &H15)},
       {"EntriesAmount", New KeyValuePair(Of Integer, Integer)(&H18, &H19)}
   } ' Friendly-Name, {Start-Offset, End-Offset}

#End Region

#Region " Properties "

   ''' <summary>
   ''' Gets the raw byte-data of the BMD file.
   ''' </summary>
   Public ReadOnly Property RawData As Byte()
       Get
           Return Me.rawDataB
       End Get
   End Property
   Private ReadOnly rawDataB As Byte()

   ''' <summary>
   ''' Gets the Chunk ID of the file format.
   ''' </summary>
   Public ReadOnly Property ChunkID As Byte
       Get
           Return Me.chunkIDB
       End Get
   End Property
   Private chunkIDB As Byte

   ''' <summary>
   ''' Gets the size of the file.
   ''' </summary>
   Public ReadOnly Property Size As Short
       Get
           Return Me.sizeB
       End Get
   End Property
   Private sizeB As Short

   ''' <summary>
   ''' Gets the Magic Identifier of the file format.
   ''' </summary>
   Public ReadOnly Property MagicIdentifier As Byte()
       Get
           Return Me.magicIdentifierB
       End Get
   End Property
   Private magicIdentifierB As Byte()

   ''' <summary>
   ''' Gets the end of text table of the file format.
   ''' </summary>
   Public ReadOnly Property EndOfTextTable As Short
       Get
           Return Me.endOfTextTableB
       End Get
   End Property
   Private endOfTextTableB As Short

   ''' <summary>
   ''' Gets the amount of offsets in offset table of the file format.
   ''' </summary>
   Public ReadOnly Property OffsetsAmount As Short
       Get
           Return Me.offsetsAmountB
       End Get
   End Property
   Private offsetsAmountB As Short

   ''' <summary>
   ''' Gets the amount of entries of the file format.
   ''' </summary>
   Public ReadOnly Property EntriesAmount As Short
       Get
           Return Me.entriesAmountB
       End Get
   End Property
   Private entriesAmountB As Short

#End Region

#Region " Constructors "

   Private Sub New()
   End Sub

   Public Sub New(ByVal filepath As String)
       Me.New(File.ReadAllBytes(filepath))
   End Sub

   Public Sub New(ByVal file As FileInfo)
       Me.New(file.FullName)
   End Sub

   Public Sub New(ByVal raw As Byte())
       Me.rawDataB = raw
       Me.ReadHeader()
   End Sub

#End Region

#Region " Private Methods "

   ''' <summary>
   ''' Reads the BMD header.
   ''' </summary>
   Private Sub ReadHeader()

       Dim chunkID As KeyValuePair(Of Integer, Integer) = Me.HeaderOffsets("ChunkID")
       Dim size As KeyValuePair(Of Integer, Integer) = Me.HeaderOffsets("Size")
       Dim magicIdentifier As KeyValuePair(Of Integer, Integer) = Me.HeaderOffsets("MagicIdentifier")
       Dim endOfTextTable As KeyValuePair(Of Integer, Integer) = Me.HeaderOffsets("EndOfTextTable")
       Dim offsetsAmount As KeyValuePair(Of Integer, Integer) = Me.HeaderOffsets("OffsetsAmount")
       Dim entriesAmount As KeyValuePair(Of Integer, Integer) = Me.HeaderOffsets("EntriesAmount")

       Me.magicIdentifierB = Me.GetBytes(Me.HeaderOffset, magicIdentifier.Key, magicIdentifier.Value)
       Me.chunkIDB = Me.GetByte(Me.HeaderOffset, chunkID.Key)
       Me.sizeB = BitConverter.ToInt16(Me.GetBytes(Me.HeaderOffset, size.Key, size.Value), 0)
       Me.endOfTextTableB = BitConverter.ToInt16(Me.GetBytes(Me.HeaderOffset, endOfTextTable.Key, endOfTextTable.Value), 0)
       Me.offsetsAmountB = BitConverter.ToInt16(Me.GetBytes(Me.HeaderOffset, offsetsAmount.Key, offsetsAmount.Value), 0)
       Me.entriesAmountB = BitConverter.ToInt16(Me.GetBytes(Me.HeaderOffset, entriesAmount.Key, entriesAmount.Value), 0)

       Me.TableTextOffset = Me.TableNonTextOffset + (8 * (entriesAmountB - 1)) + 2

       Me.ReadNonTextTable()
       Me.ReadTextTable()

   End Sub

   ''' <summary>
   ''' Reads the non-text table of the BMD file format.
   ''' <para></para>
   ''' This table seems not useful.
   ''' </summary>
   Private Sub ReadNonTextTable()

       For x As Integer = 0 To (entriesAmountB - 1)

           Dim offset As Integer = Me.TableNonTextOffset + (8 * x)
           Dim data As Byte() = Me.GetBytes(offset, 0, 1)
           Dim value As Short = BitConverter.ToInt16(data, 0)

#If DEBUG Then
           Debug.WriteLine(String.Format("Entry.Index.: {0}", (x + 1)))
           Debug.WriteLine(String.Format("Start.Offset: DEC={0,-4} HEX=0x{1}", offset, offset.ToString("X")))
           Debug.WriteLine(String.Format("Raw.Bytes...: {0}", String.Join(" ", From b As Byte In data Select b.ToString("X"))))
           Debug.WriteLine(String.Format("Int16.Value.: {0}", value))
           Debug.WriteLine(String.Empty)
#End If

       Next

   End Sub

   ''' <summary>
   ''' Reads the text table of the BMD file format.
   ''' <para></para>
   ''' This table contains the items identifiers and their descriptions for further translation.
   ''' </summary>
   Private Sub ReadTextTable()

       For x As Integer = 0 To ...
           ' ...?
       Next

   End Sub

   Private Function GetByte(ByVal start As Integer, ByVal offset As Integer) As Byte
       Return Buffer.GetByte(array:=Me.RawData, index:=start + offset)
   End Function

   Private Function GetBytes(ByVal start As Integer, ByVal from As Integer, ByVal [to] As Integer) As Byte()
       Return Me.rawDataB.Skip(start + from).Take(([to] - from) + 1).ToArray()
   End Function

#End Region

End Class


Saludos!








ivancea96

#1
Obtengo estos datos:
boots_2000
boots_2001
boots_2002
boots_2003
boots_2004
boots_2005
boots_2006
boots_2007
boots_2008
boots_2009
boots_200A
boots_200B
boots_200C
boots_200D
boots_200E
boots_200F
boots_2010
boots_2011
boots_2012
boots_2013
boots_2014
boots_2015
boots_2016
boots_2017
boots_2018
boots_2019
boots_201A
boots_201B
boots_201C
boots_201D
boots_201E
boots_201F
boots_2020
boots_2021
boots_2022
boots_2023
boots_2024
boots_2025
boots_2026
boots_2027
boots_2028
boots_2029
boots_202A
boots_202B
boots_202C
boots_202D
boots_202E
boots_202F
boots_2030
boots_2031
boots_2032
boots_2033
boots_2034
boots_2035
boots_2036
boots_2037
boots_2038
boots_2039
boots_203A
boots_203B
boots_203C
boots_203D
boots_203E
boots_203F
boots_2040
boots_2041
boots_2042
boots_2043
boots_2044
boots_2045
boots_2046
boots_2047
boots_2048
boots_2049
boots_204A
boots_204B
boots_204C
boots_204D
boots_204E
boots_204F
boots_2050
boots_2051
boots_2052
boots_2053
boots_2054
boots_2055
boots_2056
boots_2057
boots_2058
boots_2059
boots_205A
boots_205B
boots_205C
boots_205D
boots_205E
boots_205F
boots_2060
boots_2061


Con este código en C++:
Código (cpp) [Seleccionar]
#include <fstream>
#include <iostream>
#include <vector>

using namespace std;

struct BMDHeader{
uint8_t chunkId;
uint8_t __pad0[3];
uint16_t fileSize;
uint8_t __pad1[1];
uint32_t magicIdentifier ;
uint32_t _null_ : 32;
uint16_t textTableEnd;
uint8_t __pad2[2];
uint16_t offsetAmount;
uint8_t __pad3[2];
uint8_t entryAmount;
uint8_t __pad4[3];
uint32_t _20000_;
};

int main(){
   /// READ FILE
fstream inFile("datBootsHelp.bmd", ios::binary | ios::in);
if(!inFile){
cout << "File error" << endl;
return 1;
}

inFile.seekg(0, ios::end);
string file(inFile.tellg(), '\0');
inFile.seekg(0);

inFile.read(const_cast<char*>(file.data()), file.size());
   /// FILE READED
   
   /// GETTING HEADER
BMDHeader header = *(BMDHeader*)file.data();
if(header._20000_ != 0x20000 || header.fileSize != file.size()){
       cout << "Header error" << endl;
       return 2;
}
   /// HEADER OK

cout << (int)header.entryAmount << " entries" << '\n' << endl;

   /// READING VALUES
   vector<string> entries(header.entryAmount);

   for(int i=0; i<header.entryAmount; i++){
       uint16_t offset = *(uint16_t*)&file[0x24 + 8 * i];
       entries[i] = string(&file[0x20 + offset]);
   }

   for(string& str :  entries){
       cout << str << '\n';
   }

   cout << '\n' << "ENDED" << endl;
}


Eso sí, ignoré gran parte de los campos del header. No sé si son necesarias.

Y ya podías decir al menos de qué trataba el formato eh? Que tuve que descubrirlo xD http://datacrystal.romhacking.net/wiki/Persona_3_and_4/BMD_(File_Format)


EDITO:
Un poco hardcodeado para obtener los datos que faltan (a parte del ID):

Código (cpp) [Seleccionar]
#include <fstream>
#include <iostream>
#include <map>

using namespace std;

struct BMDHeader{
uint8_t chunkId;
uint8_t __pad0[3];
uint16_t fileSize;
uint8_t __pad1[1];
uint32_t magicIdentifier ;
uint32_t _null_ : 32;
uint16_t textTableEnd;
uint8_t __pad2[2];
uint16_t offsetAmount;
uint8_t __pad3[2];
uint8_t entryAmount;
uint8_t __pad4[3];
uint32_t _20000_;
};

int main(){
   /// READ FILE
fstream inFile("datBootsHelp.bmd", ios::binary | ios::in);
if(!inFile){
cout << "File error" << endl;
return 1;
}

inFile.seekg(0, ios::end);
string file(inFile.tellg(), '\0');
inFile.seekg(0);

inFile.read(const_cast<char*>(file.data()), file.size());
   /// FILE READED

   /// GETTING HEADER
BMDHeader header = *(BMDHeader*)file.data();
if(header._20000_ != 0x20000 || header.fileSize != file.size()){
       cout << "Header error" << endl;
       return 2;
}
   /// HEADER OK

cout << (int)header.entryAmount << " entries" << '\n' << endl;

   /// READING VALUES
   map<string, string> entries;

   for(int i=0; i<header.entryAmount; i++){
       uint16_t offset = *(uint16_t*)&file[0x24 + 8 * i];
       string id = string(&file[0x20 + offset]);
       entries[id] = string(&file[0x20 + offset + id.size() + 0x20]);
   }

   for(auto& p :  entries){
       cout << p.first << ":\n";
       cout << p.second << "\n\n";
   }

   cout << '\n' << "ENDED" << endl;
}

Datos:
boots_2000:
Reserve


boots_2001:
Solidly made
work boots.


boots_2002:
An ordinary pair of
loafers.


boots_2003:
Long, black boots.


boots_2004:
Boots with a horizontal
line over the toes.


boots_2005:
Classic-model sneakers.


boots_2006:
Shoes for playing
indoor soccer.


boots_2007:
Low-heeled boots.


boots_2008:
Sandals that massage
your pressure points.


boots_2009:
Strong enough to take
any punishment.


boots_200A:
Boots with thick soles.


boots_200B:
Sandals with many
bumps.


boots_200C:
Boots made out of
leather.


boots_200D:
Shoes with a single
row of wheels.


boots_200E:
Geta made with the
latest technology.


boots_200F:
Boots from military
surplus.


boots_2010:
Greaves colored
a dark red.


boots_2011:
Sandals that boost
the wearer's magic.


boots_2012:
Shoes made with
modern technology.


boots_2013:
Sandals worn by
ninjas.


boots_2014:
Shoes with needles
on the soles.


boots_2015:
Boots with jet engines
attached.


boots_2016:
Exceptionally durable
greaves.


boots_2017:
Very light shoes.


boots_2018:
Sandals with dazzling
silver ornaments.


boots_2019:
0x2019


boots_201A:
0x201A


boots_201B:
Shoes with pentacle
symbols.


boots_201C:
Hitmen's favorite
shoes.


boots_201D:
Unbelievably light
pair of sandals.


boots_201E:
A war god's spirit
dwells in these boots.


boots_201F:
Boots that have been
blessed.


boots_2020:
Women's boots that
lend beauty.


boots_2021:
A famous ninja's
sandals.


boots_2022:
0x2022


boots_2023:
0x2022


boots_2024:
Men's boots embroidered
with dragons.


boots_2025:
Can supposedly be
remote-controlled...


boots_2026:
0x2026


boots_2027:
A manly pair of
geta, for men.


boots_2028:
Shoes with vicious-
looking spikes.


boots_2029:
0x2029


boots_202A:
Sandals with a lion
drawn on them.


boots_202B:
Men's greaves, worn
by a veteran warrior.


boots_202C:
0x202C


boots_202D:
0x202D


boots_202E:
Sandals with a fresh
grass scent.


boots_202F:
0x202F


boots_2030:
0x2030


boots_2031:
0x2031


boots_2032:
Savior's boots worn
only by men.


boots_2033:
0x2033


boots_2034:
Leopard-print leggings
for women.


boots_2035:
0x2035


boots_2036:
Lightweight pumps
for women.


boots_2037:
0x2037


boots_2038:
Heels that make women
supersexy.


boots_2039:
0x2039


boots_203A:
0x203A


boots_203B:
0x203B


boots_203C:
Women's boots that
last forever.


boots_203D:
The paw pads are very
soft to the touch.


boots_203E:
0x203E


boots_203F:
0x203F


boots_2040:
0x2040


boots_2041:
0x2041


boots_2042:
0x2042


boots_2043:
0x2043


boots_2044:
0x2044


boots_2045:
0x2045


boots_2046:
0x2046


boots_2047:
Ordinary leg parts for
Aigis.


boots_2048:
Leg parts for Aigis made
with strong fibers.


boots_2049:
Processed iron leg parts
for Aigis.


boots_204A:
Molecularly cohesive leg
parts for Aigis.


boots_204B:
Ceramic leg parts for
Aigis.


boots_204C:
Cobalt leg parts for
Aigis.


boots_204D:
Lightweight leg parts for
Aigis.


boots_204E:
Alloyed leg parts for
Aigis.


boots_204F:
Consolidated leg parts
for Aigis.


boots_2050:
Leg parts for Aigis from
the Beast God.


boots_2051:
2051


boots_2052:
2052


boots_2053:
Shining leg parts for
Metis.


boots_2054:
Leather-covered leg parts
for Metis.


boots_2055:
Resin leg parts for Metis.


boots_2056:
Light ceramic leg parts
for Metis.


boots_2057:
Light alloy leg parts for
Metis.


boots_2058:
Titanium leg parts for
Metis.


boots_2059:
Zirconium leg parts for
Metis.


boots_205A:
Imitation dancing shoes
for Metis.


boots_205B:
Magical leg parts for
Metis.


boots_205C:
Crimson leg parts for
Metis.


boots_205D:
Angelic leg parts for
Aigis.


boots_205E:
Mysterious glass leg
parts for Metis.


boots_205F:
The ultimate prototype
leg parts for Aigis.


boots_2060:
Supreme pitch-black leg
parts for Metis.


boots_2061:
ü·ü╩ü┘


Tengo que ver cómo obtener los datos, ya que no parece que hablen de ello en lo del formato.

EDITO:
Mejorado. Ahora capta bien el mensaje (salvo para el último, que no parece tener el mismo formato (o el formato está mal interpretado))

Y con respecto a lo que dices de cuándo acaba la primera tabla y cuándo empieza la segunda, yo me limité a lo que dice esa web y utilizar directamente el offset "Text offset relative from offset 20". Sumarlo, para saber la posición de la celda de texto, y listo. Iterar por cada elemento de la primera tabla, y se acabó.

Eleкtro

#2
@ivancea96
Joder, no me esperaba una ayuda tan gratificante, ¡muchísimas gracias!. Más tarde me pondré a analizar el código e intentar reproducirlo en VB.NET.

PD: No quise decir de que trataba el formato BMD por cuestiones éticas, para no dar motivo a que se cierre el post, de todas formas en el código que compartí, arriba en la cabecera habia una url a esa wiki... xD.

EDITO: Veo que la cantidad de identificadores de items (boots_*) que obtuviste son 98, y el valor "98" se puede obtener en la cabecera del formato, y además el 98 también hace referencia a la cantidad de valores que hay en la primera tabla, así que deduzco que esos 98 valores de la primera tabla deberán especificar la longitud de cada bloque de la segunda tabla, genial!

Saludos y gracias de nuevo por tu tiempo!








Eleкtro

#3
Hola. Dejé a un lado este proyecto por un tiempo y hoy he vuelto a ponerme. @ivancea96, estoy adaptando tu código, pero tengo una duda...

¿Cómo determinas la longitud del campo "id" (9 caracteres legibles en este caso, 42 de espacio en total creo) y del texto/descripción del item?.

En esta iteración no veo que determines la longitud de "id" ni tampoco de "entries[0]", ¿como es posible entonces?, no lo entiendo.

Código (cpp) [Seleccionar]
   for(int i=0; i<header.entryAmount; i++){
       uint16_t offset = *(uint16_t*)&file[0x24 + 8 * i];
       string id = string(&file[0x20 + offset]);
       entries[id] = string(&file[0x20 + offset + id.size() + 0x20]);
   }


En serio, ¿como determinas el tamaño que tiene "id" y cada elemento del array "entries"?.

EDITO: Estoy leyendo una referencia online de C++, no se si esto tiene algo que ver pero creo que cuando usas string file(inFile.tellg(), '\0'); devuelve la posición del caracter evaluado "\0" (¿está \escapado?, no me queda muy claro si eso es un "0", no se nada de c/c++).




Esta sería mi adaptación de tu iteración, INACABADA... por que desconozco la longitud de esos campos.
Código (vbnet) [Seleccionar]
For x As Integer = 0 To (entryAmountB - 1)

   Dim offset As Integer = Me.TableNonTextOffset + (8 * x)
   Dim value As Short = BitConverter.ToInt16(Me.GetBytes(offset, 0, 1), 0)

   Dim idOffset As Short = (&H20 + value)
   Dim id As String = Encoding.UTF8.GetString(Me.GetBytes(idOffset, 0, LONGITUD_DE_ID))

   Dim textOffset As Short = (idOffset + id.Length() + &H20)
   Dim text As String = Encoding.UTF8.GetString(Me.GetBytes(textOffset, 0, LONGITUD_DE_TEXTO))

   Debug.WriteLine(String.Format("Entry.Id...: {0}", id))  ' boots_nnnn
   Debug.WriteLine(String.Format("Entry.Text.: {0}", text)) ' el texto/descripción del item

Next


Cada texto de "boots_nnnn" termina con una secuencia de bytes parecida a estas:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 FF FF 44 03 00 00 0F 00 00 00 F2 08 FF FF F1 3F
00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 FF FF 78 03 00 00 20 00 00 00 F2 08 FF FF F1 3F
00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 FF FF C4 05 00 00 2D 00 00 00 F2 08 FF FF F1 3F


Despues de esa secuencia comienza el texto/descripción del item, y ese texto siempre parece terminar con la secuencia de bytes 2E 0A.

Sabiendo eso, creo que puedo determinar facilmente la longitud de cada campo, pero me gustaría comprender como lo haces en tu código, por que pareces conocer la longitud de forma más simple y eficiente.

Saludos!








ivancea96

el constructor de la clase string en C++, si no le pasas un tamaño, coge hasta el próximo caracter nulo ('\0'). Si no recuerdo mal, ese campo es de tamaño variable, y el resto de la memoria, la no utilizada, está rellena con caracteres nulos.
Así que eso, dura ahsta el próximo caracter nulo.