本文旨在说明修改游戏存档的思路、编程方法和一点技巧,并无其他不良企图。如果仅仅为了修改游戏,FPE、金山游侠等更为专业。
  前言
  大多数程序员都玩过游戏,也或曾想过修改游戏,笔者也不例外。我通常不希望自己受困于游戏中的经验值、金钱之类的,于是采用修改游戏存档文件的方法,自己动手修改比起使用金山游侠等更有乐趣。毕竟有时候只要享受一下游戏的情节就够了,把大量的时间花费在增加经验值、赚钱方面太不合算了,毕竟时间有限而游戏无限!方法嘛,使用老牌的UltraEdit(以下简称UE),当然还需要配合“计算器”进行十进制和十六进制的转换。时间长了,也觉得繁琐,何不自己动手写一个针对游戏存档文件的修改器而一劳永逸?笔者比较喜欢C++,如果你有一定的C++基础,跟我走吧!
  笔者的电脑:AMD XP1700+,Windows2000(sp4),Borland C++ Builder 6(sp4)
  手工修改游戏存档文件的方法
  游戏存档文件大多使用二进制格式,这样对于读取和保存数据都比较方便。可使用Windows的“计算器” 来看看10进制和16进制的区别:采用“科学性”模式,在10进制模式下输入数据,然后切换到16进制就行了。
  不过就算这样转换,看起来还是不很直观,因为在游戏存档中并不是如此显示的。
  那么用C++如何表达的呢?下面这个小程序演示了如何读写二进制整数。
#include <iostream>
#include <fstream>
using namespace std;//标准库所在的空间
int main()
{
 fstream BinFile("",ios::in | ios::out | ios::binary);//读+写+二进制模式
 int i=1234;
 BinFile.write(reinterpret_cast<const char*>(&i),sizeof(int));
 //reinterpret_cast是C++的强制转换,这里把整数的地址强制转换为const char*,
 //与C 的(const char*)&i 作用相同,但是reinterpret_cast更加含义明确。
 i=0;
 BinFile.seekg(0,ios::beg);//重新指向文件开头准备读取
 ad(reinterpret_cast<char*>(&i),sizeof(int));
 cout<<"i="<<i<<’\n’;
}
  用UE打开切换到二进制模式,是这样子的:
  在计算器中看到的是04D2,在UE 中看到的是D204,这就是笔者所谓的不直观性。因此,如果你要在某个游戏存档文件中间(扩充开来就是二进制文件)寻04D2这个数值,到上图显示的地方就对了。笔者初期手工修改存档也是这样的,比较麻烦。
  下面这个小程序表明了模拟UE在二进制文件中寻
  整数的原理:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
 fstream BinFile("",ios::in | ios::out |
ios::binary);//读+写+二进制模式
 const int i=87654;
 BinFile.write(reinterpret_cast<const char*>(&i),
 sizeof(int));//强制转换,把i用二进制方式写入文件
 BinFile.seekg(0,ios::beg);
 //重新指向文件开头,准备读取
 char ch;
 ad(&ch,sizeof(char)))//读取所有字符
  cout<<static_cast<int>(ch)<<"\t";//显示
  //static_cast是C++的静态转换,与C的(int)ch作用相
  //同,但是static_cast意思表达更清楚。
  cout<<’\n’;
  //下面把i的地址转换为字符串地址,并用char方式依次读取,主要是比较两者读取的结果是否相同.
 const char* P=reinterpret_cast<const char*>(&i);
 for(int i=0;i<sizeof(int);++i)
  cout<<static_cast<int>(P[i])<<"\t";
}
用C++制作自己的游戏修改器(上b)vc++ 2007-11-29 16:27:05 阅读13 评论0 字号:大中小
手工在存档文件中使用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:\t";
 CIN>>FileName;
 BinaryFile.open(FileName.c_str(),ios::in | ios::binary);
 if(!BinaryFile){
  InputIsOk=false;
  cerr<<"Open file failed.\n";
  return;
 }
 cout<<"The integer you want to search:\t";
 CIN>>OldData;
 cout<<"Byte number(1--"<<CheckBinaryFile<T>::MaxByte<<"):\t";
 CIN>>ByteNumber;
 if(ByteNumber<1 || ByteNumber>CheckBinaryFile<T>::MaxByte) {
  //字节数错误,调整为最大值
  ByteNumber=CheckBinaryFile<T>::MaxByte;
  cout<<"Byte number was amended to " << CheckBinaryFile<T>::ByteNumber<<’\n’;
 }
}
  提示用户输入二进制存档文件,用只读+二进制模式开启。如果失败,设置输入状态为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);
 ad(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));
  //读入一个新的
  ad(&Range[ByteNumber-1],CharSize);
  ++Addr;
 }
 return Occurs;
}
  检查输入的二进制文件中有多少个OldData,并保存地址,用模拟二进制方式比较OldData。Range 是一个比较区域,这里不打算输出这个字符串,也不考虑用strcpy来拷贝内容,所以不必预留一个空间来保存结尾符号’\0’。填满Range 后,开始一个一个字符比较了:
  当Range和OldData完全相同就表示匹配成
功(memcmp返回0 表示成功),一旦成功,就把该地址保存下来(AddressList)。不管是否成功,把Range去掉一个最早读取的,然后读入一个新的,继续匹配。函数返回匹配的个数。
用C++制作自己的游戏修改器(上c)vc++ 2007-11-29 16:28:28 阅读7 评论0 字号:大中小
 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.\n";
 if(Occurs==0) return;
 cout<<"Save address info to files(y/n)?\t";
 char YN;
 CIN>>YN;
 if(YN==’y’ || YN==’Y’){
  cout<<"Address file name:\t";
  string AddressFileName;
  CIN>>AddressFileName;
  ofstream Save(AddressFileName.c_str(),ios::out);
  if(!Save)
  { cerr<<"Create "<<AddressFileName<<" failed.\n";}
  else
  { SaveAddressToFile(Save);
  Save.close();
 }
}
cout<<"Modify binary file automatically(y/n)?\t";
CIN>>YN;
 if(YN==’y’ || YN==’Y’){
  cout<<"New value:\t";
  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(),d(),
 ostream_iterator<T>(os,"\t"));
  把AddressList的内容保存下来。c
opy是C++的函数,把一个区间的内容拷贝到另一个地方。
template<class T>
void CheckBinaryFile<T>::AutoModifySave(const T& NewValue)
const
{
 list<AddressType>::const_iterator Beg=AddressList.
 begin(),d();
 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.\n";
   continue;
  }
  while(Addr < *Beg && BinaryFile){
   //小于指定地址的内容
   ad(&ch,CharSize);
   Write.write(&ch,CharSize);
   ++Addr;
  }
  for(int k=0;k<ByteNumber;++k){//忽略源文件
   ad(&ch,CharSize);
  }
  Write.write(P,CharSize*ByteNumber); //写入新值
  while(BinaryFile){//源文件剩余的内容拷贝到新文件
   ad(&ch,CharSize);
   Write.write(&ch,CharSize);
  }
  Write.close();
  cout<<NewFileName<<" ... successfully.\n";
 }//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++制作自己的游戏修改器(上d)vc++ 2007-11-29 16:30:12 阅读4 评论0 字号:大中小
应该说有些游戏的存档还是很老实的——地址不变。
  对于这种类型的存档,我们可以用对集合取交集的方法来缩小范围。比如经验值为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.\n";
 string fn;
 CIN>>fn;
 ifstream Read(fn.c_str());
 if(!Read){
  cerr<<"Open "<<fn<<" failed.\n";
  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.\n";
  return;
 }
mutable是什么意思 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的交集
 pty()==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()

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。