龙盟编程博客 | 无障碍搜索 | 云盘搜索神器
快速搜索
主页 > 软件开发 > C/C++开发 >

用C++制作自己的游戏修改器(上)[组图]

时间:2009-12-22 15:42来源:未知 作者:admin 点击:
分享到:
本文旨在说明修改游戏存档的思路、编程方法和一点技巧,并无其他不良企图。假如仅仅为了修改游戏,FPE、金山游侠等更为专业。 前言 更多内容请看C/C++技术专题 网络游戏攻略 游戏

  本文旨在说明修改游戏存档的思路、编程方法和一点技巧,并无其他不良企图。假如仅仅为了修改游戏,FPE、金山游侠等更为专业。

  

  前言

  

  

更多内容请看C/C++技术专题 网络游戏攻略 游戏策划专题,或

  

自动检查游戏存档中的数值

  

  手工在存档文件中使用UE中来查找某个数值的时候,可能找到好多地方,靠一个一个查找然后记录下地址可真费眼神。写个程序来自动寻找指定的数值,并且记录下地址吧!本文所述的地址都是从0开始的,而且都以十进制方式输入输出。

  

  

template<class T>

  class CheckBinaryFile

  {

   public:

  typedef fstream::off_type AddressType;

  CheckBinaryFile();

  void Run();

   private:

  static const int MaxByte=sizeof(T);

  const int CharSize;

  EInputStream CIN;//我自己写的一个加强输入流

  string FileName;

  T OldData;

  int ByteNumber;

  mutable bool InputIsOk;

  mutable ifstream BinaryFile;

  mutable list<AddressType> AddressList;

  void Input();

  int Check() const;

  void SaveAddressToFile(ostream&) const;

  void AutoModifySave(const T&) const;

  };

  template<class T>

  const int CheckBinaryFile<T>::MaxByte;//定义静态整型常量

  这是自己定义的一个类,下面逐一解释:

  

  

template<class T>

  T代表要寻找的数据的类型。当然,这个程序只是寻找整数(经验值、金钱都是整数!),但我不排除以后要查找其他类型的数据。为了可扩充性,使用了模板。

  

  

typedef fstream::off_type AddressType;

  我要找到数据在文件中总有地址,这个地址是什么类型呢? int还是long,或者是其他类型?fstream有一个类型叫off_type,应该是偏移类型的含义,在这里我把这个类型叫做AddressType。

  

  

static const int MaxByte=sizeof(T);

  这是一个静态整型常量,表示T的大小(最多有多少字节),比如在我的机器上,sizeof(int)=4。T的大小在编译的时候就确定,而且它不能被修改(const),对于所有查找类型相同的CheckBinaryFile,这个数值是唯一的,共享的(static)。

  

  构造函数:

  

  

template<class T>

  CheckBinaryFile<T>::CheckBinaryFile():CharSize(sizeof (char)),CIN(cin)

  { InputIsOk=true; Input(); }

  CharSize 为sizeof(char),把cin 绑定到CIN。由于CharSize是常量,必须在构造函数的初始化列表中设定。

  

  预设输入状态,调用输入函数:

  

  

template<class T>

  void CheckBinaryFile<T>::Input()

  {

   cout<<"Binary file name:";

   CIN>>FileName;

   BinaryFile.open(FileName.c_str(),ios::in ios::binary);

   if(!BinaryFile){

  InputIsOk=false;

  cerr<<"Open file failed.

";

  return;

   }

   cout<<"The integer you want to search:";

   CIN>>OldData;

   cout<<"Byte number(1--"<<CheckBinaryFile<T>::MaxByte<<"):";

   CIN>>ByteNumber;

   if(ByteNumber<1 ByteNumber>CheckBinaryFile<T>::MaxByte) {

  //字节数错误,调整为最大值

  ByteNumber=CheckBinaryFile<T>::MaxByte;

  cout<<"Byte number was amended to " << CheckBinaryFile<T>::ByteNumber<<’

’;

   }

  }

  提示用户输入二进制存档文件,用只读+二进制模式开启。假如失败,设置输入状态为false,直接退出。然后提示用户输入要查找的整数(OldData)以及多少个字节(ByteNumber)。假如字节数错误,调整为最大值。由于计算机系统的不同以及char,short,int,long之间存在

  转换关系,对于某些整型的字节数是不可确定的。比如100,可以用char表示,那么只需要sizeof(char)个字节表示就够了,当然也可以用字节数更多的类型,比如int,来表示100。

  

  

template<class T>

  int CheckBinaryFile<T>::Check() const{

   const char* P=reinterpret_cast<const char*>(&OldData);

   char Range[CheckBinaryFile<T>::MaxByte];

   int Occurs=0;

   AddressType Addr=0;

   //填充0

   memset(Range,0,CheckBinaryFile<T>::MaxByte*CharSize);

   BinaryFile.read(Range,CharSize*ByteNumber);//填满Range

   while(BinaryFile){

  if(memcmp(P,Range,CharSize*ByteNumber)==0){//匹配成功

   AddressList.push_back(Addr);

   ++Occurs;

  }

  //删除一个最旧的

  memcpy(Range,&Range[1],CharSize*(ByteNumber-1));

  //读入一个新的

  BinaryFile.read(&Range[ByteNumber-1],CharSize);

  ++Addr;

   }

   return Occurs;

  }

  

  检查输入的二进制文件中有多少个OldData,并保存地址,用模拟二进制方式比较OldData。Range 是一个比较区域,这里不打算输出这个字符串,也不考虑用strcpy来拷贝内容,所以不必预留一个空间来保存结尾符号’\0’。填满Range 后,开始一个一个字符比较了:

  

  当Range和OldData完全相同就表示匹配成功(memcmp返回0 表示成功),一旦成功,就把该地址保存下来(AddressList)。不管是否成功,把Range去掉一个最早读取的,然后读入一个新的,继续匹配。函数返回匹配的个数。 更多内容请看C/C++技术专题 网络游戏攻略 游戏策划专题,或

  

  list是标准C++的一个容器,类似双向链表,在添加/删除节点方面表现优秀。我不打算使用排序,因为从头到尾遍历文件时保存下来的地址肯定是有序的;我也不需要随机读取这些地址,所以排除了vector以及deque这两种容器。至于没有采用内建的数组,咳,我不

  知道能找到多少地址,或许一个都没有,或许成千上万。

  

  list有一个size()函数,望文生义就是大小的意思,的确如此。不过由于list是一种链表,不像数组那样只要把头尾指针相减就能得到大小,取得size的办法只有从头到尾走一遍,速度比较慢。既然这个函数很清楚取得了多少个地址,那就直接返回这个数目吧!

  

  

template<class T>

  void CheckBinaryFile<T>::Run()

  {

   if(InputIsOk==false) return;

   const int Occurs=Check();

   cout<<Occurs<<" different addresses were found.

";

   if(Occurs==0) return;

   cout<<"Save address info to files(y/n)?";

   char YN;

   CIN>>YN;

   if(YN==’y’ YN==’Y’){

  cout<<"Address file name:";

  string AddressFileName;

  CIN>>AddressFileName;

  ofstream Save(AddressFileName.c_str(),ios::out);

  if(!Save)

  { cerr<<"Create "<<AddressFileName<<" failed.

";}

  else

  { SaveAddressToFile(Save);

  Save.close();

   }

  }

  cout<<"Modify binary file automatically(y/n)?";

  CIN>>YN;

   if(YN==’y’ YN==’Y’){

  cout<<"New value:";

  T NewValue;

  CIN>>NewValue;

  system("dir > @tmp");

  system("del @*/q");

  AutoModifySave(NewValue);

   }

  }

  假如输入错误,则直接退出。显示匹配的个数并询问是否保存这些地址至文件。再询问是否自动修改。比如找到了10个地址,自动修改将产生10个新文件,每个文件与原文件相比都只修改了一个地址的数值。输入新的数值,将产生若干个新文件。新文件的格式是@+地址的十进制表示。产生新文件前先把旧的以@开头的文件删除。假如不存在@开头的文件,system("del @*/q");会说找不到文件,不大舒适,那我先制造一个@tmp(system("dir > @tmp");),这里使用了DOS的输出重定向,把原本显示到屏幕的内容输入到@tmp中。

  

  

template<class T>

  void CheckBinaryFile<T>::SaveAddressToFile(ostream& os)

  const

  {

   copy(AddressList.begin(),AddressList.end(),

   ostream_iterator<T>(os,""));

  }

  把AddressList的内容保存下来。copy是C++的函数,把一个区间的内容拷贝到另一个地方。

  

  

template<class T>

  void CheckBinaryFile<T>::AutoModifySave(const T& NewValue)

  const

  {

   list<AddressType>::const_iterator Beg=AddressList.

   begin(),End=AddressList.end();

   const char* P=reinterpret_cast<const char*>(&NewValue);

   for(;Beg!=End;++Beg){

  BinaryFile.clear();//清除错误状态

  BinaryFile.seekg(0,ios::beg);//指向文件开头,预备读 AddressType Addr=0;

  char ch;

  stringstream NewFile;

  NewFile<<"@"<<*Beg;

  string NewFileName(NewFile.str());

  ofstream Write(NewFileName.c_str(),ios::out ios:: binary);

  if(!Write){

   cerr<<NewFileName<<" ... unsUCcessfully.

";

   continue;

  }

  while(Addr < *Beg && BinaryFile){

   //小于指定地址的内容

   BinaryFile.read(&ch,CharSize);

   Write.write(&ch,CharSize);

   ++Addr;

  }

  for(int k=0;k<ByteNumber;++k){//忽略源文件

   BinaryFile.read(&ch,CharSize);

  }

  Write.write(P,CharSize*ByteNumber); //写入新值

  while(BinaryFile){//源文件剩余的内容拷贝到新文件

   BinaryFile.read(&ch,CharSize);

   Write.write(&ch,CharSize);

  }

  Write.close();

  cout<<NewFileName<<" ... successfully.

";

   }//for

  }

  根据AddressList的大小遍历若干遍源文件。新的文件用@+地址格式。先把小于指定地址的内容拷贝到新文件,到了指定地址后把新值写入新文件,再把源文件剩余的内容拷贝到新文件。const_iterator是常量迭代器,表明不修改AddressList 的内容。begin 函数得到 AddressList的开头,end函数得到AddressList的最后一个元素的下一个地址,++表示迭代器前进一格。把源文件剩余的内容拷贝到新文件后,会导致源文件BinaryFile 的状态为bad,在bad状态下要执行比如读写、重新指向文件某个位置等操作必须先调用clear清除这个状态。

  

  mutable是C++新近的要害字,大体意思是表明该内容可以在const成员函数中修改。比如在这个类中间,比如mutable bool InputIsOk;InputIsOk只是表明用户输入数据的正确性,并不影响自身的状态; mutable list<AddressType> AddressList;也没有改动源文件的各个属性,只是保存了信息。

  

  好了,这个类基本写完了。他的功能是:

  

  输入一个二进制文件名以及要查找的整数和字节数。

  

  告诉你找到了多少个地址(可保存地址信息到文件),假如你愿意,可以分别把这些地址上的数据修改为新的数值后产生新文件。

  

  你可以在仙剑2上做实验。仙剑2的存档地址不是固定的。记录下当前的经验值和金钱(都是4字节),存档后切换到Windows,对存档的文件开刀,假如报告找到的地址只有四五个,可以自动产生新文件。把新文件覆盖原存档,切换到游戏后读取刚刚修改的文件试试看。大

  不了直接退出游戏。仙剑2 可以直接切换到Windows,这对于修改存档比较方便。我以前老老实实玩到底才32级,现在可以一下子飙升到七八十级(最高似乎是99),我以前不知道苏媚还有“狐舞动天”的特技,嗬嗬! 更多内容请看C/C++技术专题 网络游戏攻略 游戏策划专题,或 改进1 :对地址文件取得交集

  

  应该说有些游戏的存档还是很老实的――地址不变。

  

  

  

  对于这种类型的存档,我们可以用对集合取交集的方法来缩小范围。比如经验值为4的时候存档为A,经验值为7 的时候存档为B。对A用上面的工具查找4,保存地址信息为4.txt;对B用上面的工具查找7,保存地址信息为7.txt。把4.txt和7.txt的内容看作两个集合,假如地

  址不变,那么取得两者的交集就能大大缩小查找范围。

  

  嗯,仙剑2 不行,仙剑1 和3倒是可以的。

  

  对于集合的个数,至少两个,可以对多个集合取交集。C++提供了set_intersection函数,可以对两个有序区间进行交集运算,我们只需要不断重复这个过程,就能对多个集合执行交集运算了。

  

  约定:输入若干个集合文件进行交集元算,当输入一个不存在的文件表示结束输入。当程序发现取得空集的时候就自动结束。

  

  

template<class T>

  void GetIntersection()

  {

   EInputStream CIN(cin);

   cout<<"Input some text filenames for reading,end

   with a nonexistent one.

";

   string fn;

   CIN>>fn;

   ifstream Read(fn.c_str());

   if(!Read){

  cerr<<"Open "<<fn<<" failed.

";

  return;

   }

   vector<T> V1;

   copy(istream_iterator<T>(Read),istream_iterator<T> (),back_inserter(V1));//保存file1的内容到V1

   CIN>>fn;

   Read.clear();

   Read.close();

   Read.open(fn.c_str());

   if(!Read){

  cerr<<"Open "<<fn<<" failed.

";

  return;

   }

   vector<T> V2,V3;

   copy(istream_iterator<T>(Read),istream_iterator<T> (),back_inserter(V2));//保存file2的内容到V2

   sort(V1.begin(),V1.end());//排序

   //删除重复的数据

   V1.erase(unique(V1.begin(),V1.end()),V1.end());

   sort(V2.begin(),V2.end());

   V2.erase(unique(V2.begin(),V2.end()),V2.end());

   set_intersection(V1.begin(),V1.end(),V2.begin(),

   V2.end(),back_inserter(V3));//V3=V1和V2的交集

   while(V3.empty()==false){

  //假如是空集就可以退出了

  CIN>>fn;

  Read.clear();

  Read.close();

  Read.open(fn.c_str());

  if(!Read) break;

  vector<T>().swap(V1);//清除V1

  copy(istream_iterator<T>(Read),

  istream_iterator<T>(),back_inserter(V1));

  sort(V1.begin(),V1.end());

  V1.erase(unique(V1.begin(),V1.end()),V1.end());

  V2.swap(V3);//V2和V3交换

  vector<T>().swap(V3);//清除V3

  set_intersection(V1.begin(),V1.end(),

  V2.begin(),V2.end(),back_inserter(V3));

   }

   if(V3.empty()){

  cout<<"An empty aggregate was found after reading " <<fn<<".

";

  return;

   }

   cout<<V3.size()<<" value were enumerated.

";

   cout<<"Input save filename:";

   CIN>>fn;

   ofstream Dest(fn.c_str());

   if(!Dest){

  cerr<<"Create "<<fn<<" failed.

";

   }

   else{

  copy(V3.begin(),V3.end(),ostream_iterator<T>(Dest,""));

  Dest.close();

   }

  }

  下面逐一解释:

  

  template<class T>

  

  和上一例含义一样,在此代表集合元素的类别。我可以对整数集合进行交集元算,对小数、字符串组成的集合也能进行交集元算。当然我现在只用到了整数集合。

  

  CIN是我自己的一个加强类,你可以看作cin。

  

  首先打开两个指定的文件(做交集运算至少要两个集合),假如有一个失败就退出。

  

把这两个文件的内容分别放入V1 和V2。然后对V1 和V2 排序(sort),剔除重复内容(unique和erase)。对调整过的V1 和V2 执行交集,结果保存到V3。

  

  当V3不为空集的时候开始循环:读取下一个等待输入的文件。清空V1,把新的文件内容放入V1,把V3的内容拷贝到V2,清空V3,把V1 和V2 的交集放入V3。

  

  上述“把V3的内容拷贝到V2”只是表达一个意思,实际上只是把V3 和V2 做交换而已,因为V3我需要清空,并不需要真正的拷贝。把某个集合清空,只是和临时的空集做交换而已。

  

  这里我使用vector容器,set也是可以的。使用set的好处是可以自动排序和剔除重复内容,当然自动排序和保持元素的唯一性是需要代价的。使用vector的好处是等到所有输入完毕后,执行某些函数(比如sort,unique,erase)来完成上述功能,一次性达到目的,而不像set那样任何时刻都保持元素的有序性和唯一性。

  

  当数据量比较大的时候,vector或许要高效一些。当然,主观臆断不是科学精神,实践是最好的检验手段。我在这里只是随便选取了vector。一旦选择了vector,那么“清除所有内容”最好使用“与空的临时vector交换”,采用这种方法后,vector的容量也会变得尽可能的小;而假如采用clear 的方法,容量保持不变。因为vector内部也采用数组,数组就意味着一块连续的内存,一旦需求超出了容量会导致重新分配,所以vector会采用预留一部分空间的策略,避免每次增加元素都要重新分配。而set不一样,底层采用二叉树(sgi采用更严格的

  红黑树),不需要预留空间,要多少分配多少,对它进行清空操作只需要简单的执行clear即可,当然,和空的临时集合作交换也很好。临时变量一旦离开自己的生存期就会释放自身的资源。

  

  拿仙剑3举例,比如有24文钱的时候存档为pal01.arc。有60文钱时存档为pal02.arc。退出游戏(假如你有两台电脑组成网,可以不退出游戏在另外一台电脑上修改),把pal01.arc,pal02.arc和这个程序放在一起,对pal01.arc查找4 字节的24,保存地址为24.txt;对 pal02.arc 查找4 字节的60,保存地址为60.txt。然后对24.txt和60.txt做交集。仙剑3的金钱存档有两个,一个是表象,方便读取存档,另一个才是真正的存放金钱的地址。所以交集结果应该为2个。知道了真正的地址,对于自动产生的文件就可以有的放矢的选择了。

  (未完待续) 更多内容请看C/C++技术专题 网络游戏攻略 游戏策划专题,或

  

精彩图集

赞助商链接