โชคดีอีกแล้วที่เกม
Latale ไม่ได้ Encrypt ไฟล์ LDT เพราะฉะนั้นไม่จำเป็นต้อง Debug
ไฟล์ LDT เปรียบเสมือนฐานข้อมูลของเกม Latale ทั้งหมด
ไฟล์ LDT เมื่อ Unpack ออกมาจากไฟล์ SPF แล้วก็สามารถทำความเข้าใจคร่าวๆด้วย HEX Editor ได้เลย
เริ่มจากไฟล์ที่เล็กที่สุด เปิดมาก็จะหน้าตาแนวๆนี้
จะเจอ Text ที่เขียนว่า _Name และ _IconIdx อันนี้ เดาว่าน่าจะเป็น Column Name
ซึ่งแต่ละ Column Name จะมีขนาด 64 Byte
และ 12 Byte ก่อน Column Name คือ
00 00 00 00 02 00 00 00 10 00 00 00
ความรู้ทั่วไป ปกติแล้ว Binary ไฟล์ส่วนใหญ่จะต้องบอกข้อมมูลพวกนี้ ใน Header ไฟล์
- Byte ที่ไว้แสดงว่าเป็น ไฟล์ เฉพาะ ที่โปรแกรมใดๆจะอ่านได้ เช่น PK ของ Winzip , PNG ของไฟล์รูป PNG
- ขนาดของ Data
- CRC ของ Data
- รายละเอียดอื่นๆ ที่บอกถึงรายละเอียดที่สามารถนำมาประกอบการอ่าน Binary Data
-- เช่นไฟล์รูป ควรจะมี Width และ Height เป็นส่วนสำคัญ
-- ไฟล์หนัง ควรจะมี ชนิด Encode และ ชนิด Encapsulation และ จำนวน Layer หรือ ภาพกี่ Byte เสียงกี่ Byte ต่อการอ่าน 1 Blocks
แต่ไฟล์ LDT นี้ Header หลักๆ มี 3 ตัว คือ
1. [00 00 00 00] ซึ่ง ไม่รู้ว่ามันคืออะไร แต่ทุกไฟล์ LDT ส่วนใหญ่จะเป็น [00 00 00 00]
2. [02 00 00 00] เดาว่ามันเป็น จำนวน Column ของข้อมูลนี้ เพราะ เจอ แค่ 2 Column ใน Hex
3. [10 00 00 00] อันนี้อาจจะเป็นอะไรก็ได้ ตัวอย่างเช่น
-- Block size ของข้อมูลแต่ละ Rows
-- Rows Size ของข้อมูลใน ไฟล์นี้
-- แค่เลข Version เฉยๆ
-- Junk ทำไวหลอก
-- Reserved Byte เผื่อไว้ก่อน
แต่จะให้น้ำหนักไปที่ Rows Size หรือ Block Size ของข้อมูลมากกว่า เพราะคงไม่มีใครอยากออกแบบไฟล์ Binary ที่ Generate ออกมาแล้วนำกลับไปอ่านแบบ Binary ยากๆ
*กรณีที่ [10 00 00 00] ไม่ใช่ จำนวน Rows หรือ Blocks Size วิธีการอ่าน Binary นี้จะแตกต่างออกไป เพราะ การแบ่ง ข้อมูล Binary ออกเป็นส่วนๆ ต้องอาศัย ตัว Separator ที่ถูกกำหนดแล้วว่ามันจะไม่มี Byte Separator อยู่ใน ข้อมูลแน่ๆ ไม่งั้น การอ่านกลับไป จะผิด
เช่นข้อมูล
01 00 FF FF 02 00 FF FF 03 00
ตัวแบ่งข้อมูลคือ FF FF
กรณีที่ไม่มี Rows Size หรือ Block Size ก็จะอ่านไปเรื่อยๆ จนกว่าจะเจอ FF FF จึงเรื่มข้อมูลชิ้นใหม่
แต่ถ้ามีข้อมูลที่ เป็น FF FF แล้ว จะกลายเป็น
01 00 FF FF 02 00 FF FF 03 00 FF FF FF FF
การอ่าน Binary กลับมาใช้งานจะผิดทันที เพราะ โปรแกรมเข้าใจ ว่า FF FF หลังสุดคือตัวแบ่ง ข้อมูลเฉยๆ
ที่ทำให้ต้องให้นำหนัก Rows Size หรือ Block Size เพราะข้อมูลแบบนี้ ส่วนใหญ่จะออกแบบมาให้คล้ายๆตาราง มี Column และเก็บข้อมูลหลายๆ Rows
และเมื่อไล่ลงไปดูล่างๆ เห็นได้ชัดว่า จำนวน Column ไม่ได้ Flexible แต่ถูก Fixed ไว้ที่ไม่เกิน 128 Column เพราะเริ่มมีข้อมูลใหม่ ที่ Offset 0x200C
*จาก Offset 0x0C ไป ถึง 0x200C มีขนาด 0x2000 และหารด้วยขนาด MAX LENGTH ชื่อ Column = 64 ได้ 128 Column
ดูต่อไปที่ Offset 0x200C จะเห็นเป็นตัวเลข DWORD หลายๆตัว ก็เลยเดาว่าเป็น Type ของ Column
เพราะ 4 * 128(Column) = 0x220C ซึ่ง 0x220C น่าจะคือจุดเริ่มต้อของ Data พอดี
จะมีค่าระหว่าง 1 - 4
ต่อไป ดูที่ Offset 0x220C น่าจะเป็นจุดเริ่มต้นของ Data ซึ่งวิเคราะห์ คร่าวๆใน Hex Edittor แล้ว แต่ละ Rows จะมีขนาด Byte ไม่เท่ากัน ทั้งนี้เพราะ ขนาด String ที่ยาวไม่เท่ากัน
กลับไปที่ Field ที่ 3 ของ Header [10 00 00 00] ไม่ใช่ Block Size แน่ๆ จิงเหลือแค่ Rows Size เท่านั้นที่เป็นไปได้
อีกทั้ง ข้อมูลชนิด String จะมีความพิเศษกว่าข้อมูอื่นตรงที่ จะมี 2 Byte นำหน้าที่บอกขนาด ของความยาวข้อความ และตามด้วย ข้อความ โดยที่ไม่มี \0 ตามหลัง
ทำให้เพิ่มความมั่นใจเรื่อง Column Type ได้มากขึ้น เพราะ จำนวนต้องมีตัวบอกว่า Column ใดเป็นข้อมูลชนิดใด
ไฟล์ตัวอย่างตอนนี้ มี Column Type 1 และ 3 เมื่อเปรียบเทียบกับข้อมูลที่มีถึง 3 ส่วน ของ Data Row
[XX XX XX XX][YY YY][ZZ ZZ ZZ ZZ]
[01 00 00 00] [0B 00 4D 61 69 6E 74 65 6E 61 6E 63 65] [01 00 00 00]
[02 00 00 00] [06 00 4E 6F 74 69 63 65] [02 00 00 00]
แต่เมื่อลองดู LDT หลายๆ ไฟล์ จะพบว่า
[XX XX XX XX] คือ ID แต่ไม่ระบบว่าเป็น ColumnType ใด และไม่มีชื่อใน Column Name
Column Type 1 คือข้อมูลชนิด String
Column Type 2 คือข้อมูลชนิด Boolean เพราะ Data มีแค่ 1 กับ 0
Column Type 3 คือข้อมูลชนิด Signed integer
Column Type 4 คือข้อมูลชนิด Float
structure ในภาษา C จึงมีหน้าตาแบบนี้
#define MAX_COLUMN 128
#define MAX_COLUMN_NAME 64
#define OFFSET_COLUMN_NAME 0xC
#define OFFSET_COLUMN_TYPE 0x200C
#define OFFSET_DATA 0x220C
struct FILE_HEAD
{
DWORD FileHead;
DWORD ColumnSize;
DWORD RowSize;
CHAR ColumnName[MAX_COLUMN][MAX_COLUMN_NAME];
DWORD ColumnType[MAX_COLUMN]; //1 String , 2 Boolean, 3 Signed Int,4 Float
};
Code อ่านไฟล์ LDT เพื่อ Convert เป็น CSV
FILE * fp_ldt = NULL;
FILE * fp_csv = NULL;
char filename[128];
char * path1 = "DATA\\LDT\\%s.LDT";
char * path2 = "DATA\\LDT\\%s.CSV";
char * _file = "ITEM_3";
sprintf(filename,path1,_file);
PrintFError("Openfile %s ret = %s\n",filename,strerror(fopen_s(&fp_ldt,filename,"rb")));
if( fp_ldt )
{
FILE_HEAD fh_ldt;
ZeroMemory(&fh_ldt,sizeof(FILE_HEAD));
fread(&fh_ldt,sizeof(FILE_HEAD),1,fp_ldt);
PrintFError("FILE HEAD : Row %d Column %d \n",fh_ldt.RowSize,fh_ldt.ColumnSize);
fseek( fp_ldt, 0, SEEK_SET );
for(int i = 0 ; i < fh_ldt.ColumnSize && i < MAX_COLUMN ; ++i)
{
//seek to name and read
fseek( fp_ldt, OFFSET_COLUMN_NAME + (i * MAX_COLUMN_NAME), SEEK_SET );
fread(&fh_ldt.ColumnName[i],1,MAX_COLUMN_NAME,fp_ldt);
fh_ldt.ColumnName[i][MAX_COLUMN_NAME - 1] = 0;
//seek to type and read
fseek( fp_ldt, OFFSET_COLUMN_TYPE + (i * sizeof(DWORD)), SEEK_SET );
fread(&fh_ldt.ColumnType[i],1,sizeof(DWORD),fp_ldt);
PrintFError("-- Column %s Type %d \n",fh_ldt.ColumnName[i],fh_ldt.ColumnType[i]);
}
//seek to data
fseek( fp_ldt, OFFSET_DATA, SEEK_SET );
DATA_TABLE _DATA_TABLE;
WORD _TmpStringLength;
DWORD _TmpDword;
for(int i = 0 ; i < fh_ldt.RowSize ; ++i)
{
//ID
FILE_DATA_COLUMN * _COLUMN_ID = new FILE_DATA_COLUMN();
_COLUMN_ID->ColumnType = 0;
fread(&_TmpDword,1,sizeof(DWORD),fp_ldt);
_COLUMN_ID->DATA = (void*)_TmpDword;
DATA_ROW* _DATA_ROW = new DATA_ROW();
_DATA_ROW->push_back(_COLUMN_ID);
for(int j = 0 ; j < fh_ldt.ColumnSize && j < MAX_COLUMN ; ++j)
{
FILE_DATA_COLUMN * _COLUMN = new FILE_DATA_COLUMN();
_COLUMN->ColumnType = fh_ldt.ColumnType[j]; //Set Column Type
switch(_COLUMN->ColumnType)
{
case 1:
{
fread(&_TmpStringLength,1,sizeof(WORD),fp_ldt);
CHAR * _Ln = new CHAR[_TmpStringLength + 1];
fread(_Ln,1,_TmpStringLength,fp_ldt);
_Ln[_TmpStringLength] = 0;
for(int k = 0 ; k < _TmpStringLength ; ++k)
{
if( _Ln[k] == ',' )
_Ln[k] = ' ';
else if( _Ln[k] == '\n' )
_Ln[k] = ' ';
else if( _Ln[k] == '\r' )
_Ln[k] = ' ';
}
_COLUMN->DATA = _Ln;
break;
}
case 2 : case 3 : case 4:
{
fread(&_TmpDword,1,sizeof(DWORD),fp_ldt);
_COLUMN->DATA = (void*)_TmpDword;
break;
}
default:
{
PrintFError("-- Unknown Column Type \n");
break;
}
}//switch
_DATA_ROW->push_back(_COLUMN);
}//col
_DATA_TABLE.push_back(_DATA_ROW);
}//row
//Convert
sprintf(filename,path2,_file);
PrintFError("Openfile %s ret = %s\n",filename,strerror(fopen_s(&fp_csv,filename,"w+")));
if( fp_csv )
{
//Write column type
fprintf(fp_csv,"0,");
for(int j = 0 ; j < fh_ldt.ColumnSize && j < MAX_COLUMN ; ++j)
{
fprintf(fp_csv,"%d,",fh_ldt.ColumnType[j]);
}
fprintf(fp_csv,"\n");
//Write column name
fprintf(fp_csv,"_ID,");
for(int j = 0 ; j < fh_ldt.ColumnSize && j < MAX_COLUMN ; ++j)
{
fprintf(fp_csv,"%s,",fh_ldt.ColumnName[j]);
}
fprintf(fp_csv,"\n");
//Write Data
DATA_TABLE::const_iterator it_row = _DATA_TABLE.begin();
for( ; it_row != _DATA_TABLE.end() ; ++it_row )
{
DATA_ROW* _DATA_ROW = *it_row;
DATA_ROW::const_iterator it_col = _DATA_ROW->begin();
for( ; it_col != _DATA_ROW->end() ; ++it_col )
{
FILE_DATA_COLUMN * _COLUMN = *it_col;
switch(_COLUMN->ColumnType)
{
case 0:
{
fprintf(fp_csv,"%d,",(DWORD)_COLUMN->DATA);
break;
}
case 1:
{
fprintf(fp_csv,"%s,",(CHAR*)_COLUMN->DATA);
delete [] _COLUMN->DATA;
break;
}
case 2 :
{
fprintf(fp_csv,"%d,",(DWORD)_COLUMN->DATA);
break;
}
case 3 :
{
fprintf(fp_csv,"%d,",(int)_COLUMN->DATA);
break;
}
case 4 :
{
float _d = 0.0f;
DWORD _b = (DWORD)_COLUMN->DATA;
__asm
{
mov eax,_b;
//lea ebx,_d;
mov dword ptr[_d],eax
}
fprintf(fp_csv,"%0.2f,",(float)_d);
break;
}
}//switch
delete _COLUMN;
}//loop col
_DATA_ROW->clear();
delete _DATA_ROW;
fprintf(fp_csv,"\n");
}//loop row
_DATA_TABLE.clear();
fclose(fp_csv);
}
fclose(fp_ldt);
}
หน้าตา ข้อมูลที่แปลงเป็น CSV แล้ว
จากนั้นจึงแกไขข้อมูลใน CSV แล้วแปลงกลับเป็น LDT ด้วยตัวอย่าง Code นี้
FILE * fp_ldt = NULL;
FILE * fp_csv = NULL;
char filename[128];
sprintf(filename,"C:\\DATA\\LDT\\ITEM_3.CSV");
PrintFError("Openfile CSV %s ret = %s\n",filename,strerror(fopen_s(&fp_csv,filename,"rb")));
sprintf(filename,"C:\\DATA\\LDT\\ITEM_3_NEW.LDT");
PrintFError("Writefile %s ret = %s\n",filename,strerror(fopen_s(&fp_ldt,filename,"wb")));
if( fp_csv && fp_ldt )
{
FILE_HEAD fh_ldt;
ZeroMemory(&fh_ldt,sizeof(FILE_HEAD));
char _line[1024 * 10];
char* _end = new char[64];
memset((_end),0x20,64);
_end[0] = 'E';
_end[1] = 'N';
_end[2] = 'D';
//ColumnType
int _idx = 0;
if ( fgets (_line , 1024 * 10 , fp_csv) != NULL )
{
char * pch;
pch = strtok (_line,",");
pch = strtok (NULL, ","); //Cut off _ID
while (pch != NULL)
{
if( strlen(pch) > 0 )
{
fh_ldt.ColumnType[_idx++] = atoi(pch);
PrintFError ("%s\n",pch);
pch = strtok (NULL, ",");
}
}
}
fh_ldt.ColumnSize = _idx - 1;
//ColumnName
_idx = 0;
if ( fgets (_line , 1024 * 10, fp_csv) != NULL )
{
char * pch;
pch = strtok (_line,",");
pch = strtok (NULL, ","); //Cut off _ID
while (pch != NULL)
{
if( strlen(pch) > 0 )
{
memset((fh_ldt.ColumnName[_idx]),0x20,64);
StringCchCopyA((fh_ldt.ColumnName[_idx]),64,pch);
//PrintFError ("%s\n",pch);
_idx++;
pch = strtok (NULL, ",");
}
}
}
fwrite (&fh_ldt , 1 ,sizeof(FILE_HEAD) , fp_ldt );
fflush(fp_ldt);
int _row = 0;
while(!feof(fp_csv))
{
if ( fgets (_line , 1024* 10 , fp_csv) != NULL )
{
_idx = 0;
char * pch;
pch = strtok_single (_line,",");
while (pch != NULL)
{
if( _idx == 0 )
{
DWORD _ID = atoi(pch);
fwrite( &_ID ,1, 4 ,fp_ldt);
}
else
{
int _ColumnType = fh_ldt.ColumnType[_idx - 1];
if( _ColumnType == 1 )
{
WORD _STR_SIZE = (WORD)strlen(pch);
fwrite( &_STR_SIZE ,1, 2 ,fp_ldt);
fwrite( pch ,1, strlen(pch) ,fp_ldt);
}
else if( _ColumnType == 2 || _ColumnType == 3 )
{
DWORD _TEMP = (DWORD)atoi(pch);
fwrite( &_TEMP ,1, 4 ,fp_ldt);
}
else if( _ColumnType == 4 )
{
float _TEMP = (float)atof(pch);
fwrite( &_TEMP ,1, 4 ,fp_ldt);
}
}
_idx++;
pch = strtok_single (NULL, ",");
fflush(fp_ldt);
}
_row++;
}
}
fwrite( _end ,1, 64 ,fp_ldt);
fflush(fp_ldt);
fh_ldt.RowSize = _row;
fseek( fp_ldt, 0, SEEK_SET );
fwrite (&fh_ldt , 1 ,sizeof(FILE_HEAD) , fp_ldt );
fclose(fp_csv);
fclose(fp_ldt);
}