15 ม.ค. 2556

แก้ไขไฟล์ LDT ของเกม Latale

โชคดีอีกแล้วที่เกม 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);
 }



1 ความคิดเห็น:

  1. พี่ครับใจดีผมขอไฟล์เต็มๆได้ไหมครับเอา ไปใช้งาน งง มากเลย ไม่เคยจับ มาก่อนเลย c นิ ขอบคุณล่วงหน้าครับ

    ตอบลบ