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

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

时间:2009-12-22 15:42来源:未知 作者:admin 点击:
分享到:
本文旨在说明修改游戏存档的思路、编程方法和一点技巧,上篇内容分别为前言、手工修改游戏存档文件的方法、自动检查游戏存档中的数值、改进1:对地址文件取得交集等内容。本期

  本文旨在说明修改游戏存档的思路、编程方法和一点技巧,上篇内容分别为前言、手工修改游戏存档文件的方法、自动检查游戏存档中的数值、改进1:对地址文件取得交集等内容。本期向读者介绍下篇部分内容。

  

  改进2:相对值查找

  

  游戏中的某些数值并没有明确告诉你多少,但是触发事件后会增加或者减少,比如仙剑3中间女主角的好感度。游戏中间有对话,根据对话的内容可以增加对男主角的好感度。好感度影响着结局,想玩出多种结局的家伙还是“玩弄”一下女主角的感情吧!

  

  方法还是比较简单的,只要同时读取两个文件,每次读取一个字符后比较两者之差是否符合指定的相对值,假如符合的话,保留信息。

  

  

template<class T>

  void RelativeValue()

  {

   typedef fstream::off_type AddressType;

   EInputStream CIN(cin);

   string FN;

   cout<<"First binary file name:";

   CIN>>FN;

   ifstream Read1(FN.c_str(),ios::in ios::binary);

   //读+二进制模式

   if(!Read1)

   {

  cerr<<"Open "<<FN<<" failed.

";

  return;

   }

   cout<<"Second binary file name:";

   CIN>>FN;

   ifstream Read2(FN.c_str(),ios::in ios::binary);

   if(!Read2)

   {

  cerr<<"Open "<<FN<<" failed.

";

  return;

   }

   int ByteNumber;

   cout<<"Byte number:";

   CIN>>ByteNumber;//字节数

   int RV;//指定的相对值

   cout<<"Relative value(value1-value2):";

   cin>>RV;

   const int MaxByte=sizeof(T);

   const int CharSize=sizeof(char);

   if(ByteNumber<1ByteNumber>MaxByte)

  ByteNumber=MaxByte;

  T Value1,Value2;//两个文件中的数值

  char* P1=reinterpret_cast<char*>(&Value1);

  char* P2=reinterpret_cast<char*>(&Value2);

  //先全部清0

  memset(P1,0,MaxByte*CharSize);

  memset(P2,0,MaxByte*CharSize);

  AddressType Address=0;

  //填布满P1

  Read1.read(P1,CharSize*ByteNumber);

  //填布满P2

  Read2.read(P2,CharSize*ByteNumber);

  //保存信息的链表

  typedef list<pair<AddressType,pair<T,T> > > InfoList;

  InfoList IL;

  int Occurs=0;

  //当两个文件都还没有读完

  while(Read1 && Read2)

  {

   if(Value1-Value2==RV)//符合条件了

   {

  //保存信息

  IL.push_back(make_pair(Address,make_pair(Value1, Value2)));

  ++Occurs;

   }

   //除旧

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

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

   //迎新

   Read1.read(&P1[ByteNumber-1],CharSize);

   Read2.read(&P2[ByteNumber-1],CharSize);

   ++Address;

  }//while(Read1 && Read2)

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

";

  if(Occurs==0) return;

  cout<<"Input save filename:";

  CIN>>FN;

  //保存至文件

  ofstream SaveFile(FN.c_str());

  if(!SaveFile)

  {

   cerr<<"Create "<<FN<<" failed.

";

   return;

  }

  SaveFile<<"Relative value is " <<RV<<"

AddressValue1Value2

";

  for(InfoList::const_iterator Beg=IL.begin(), End=IL.end();Beg!=End;++Beg)

  {

   SaveFile<<(*Beg).first<<’’ <<(*Beg).second.first <<’’<<(*Beg).second.second <<’

’;

  }

   }

  这里的新面孔是pair。pair是一个相当简单的模板结构,一共就两个变量(first和second),都是公开的。

  

  由于是模板,所以pair的构造必须指定两个变量所属类型,为了简单,设立了一个make_pair 的函数,函数可以根据参数自动推导出类型而不必显式指定。在这里,我需要保存三个值:地址、文件1中的数值、文件2中的数值。偷懒一点,我把两个文件中的数值作为一个pair

  (A),再把地址和pair(A)作为一个pair(B)。pair(B).first就是地址,pair(B).second就是pair(A),pair(A).first就是文件1中的数值,pair(A).second就是文件2中的数值。

  

  通过保存的文件我们可以得到地址,找到了藏身之地,事情就好办多了。

  

 改进3 :手动批量修改

  

  上面介绍的方法( “自动修改文件”),每次只修改一个地址。假如要对某个文件的5555,6666,12352地址修改,用上面的方法也很烦琐。假如能指定在哪些位置修改为哪些数值就方便多了。

  

  修改的要素:地址、新的数值、新数值的字节数。上述三个要素都是整形,一旦输入数据不是整形(比如一个字母),就表示停止输入。 比如:

  

  

5555 500000 4

  6666 600000 4

  12352 700000 4 x

  表示在5555地址上填写4字节的500000,在6666地址上填写4字节的600000,在12352地址上填写4字节的700000。x导致输入失败,在此表示停止输入。

  

  假如输入的数据按照地址从小到大排列,那么编程就比较方便了,不过却为难了客户。还是善待你的上帝吧,对他们的要求越少越好。很多时候,自己就是自己程序的第一用户,益人也同样利己。

  

  因此,下面的输入与上面的等价:

  

  

12352 700000 4

  5555 500000 4

  6666 600000 4 ~

  template<class T>

  class Modify

  {

   public:

  typedef fstream::off_type AddressType;

  static const int MaxByte=sizeof(T);

  Modify();

  void Run() const;

   private:

  strUCt ModifyInfoUnit

  {

   //修改要素――地址

   AddressType Address;

   //修改要素――新的数值

   T NewValue;

   //修改要素――新数值的字节数

   int ByteNumber;

   //排序原则

   bool operator<(const Modify<T>::ModifyInfoUnit& rhs) const;

   //从输入流读取一个单元

   void ReadFrom(istream&);

  };

  const int CharSize;

  EInputStream CIN;

  void Input();

  bool InputIsOk;

  mutable ifstream SourceFile;

  mutable ofstream DestFile;

  set<ModifyInfoUnit> ModifyInfoSet;//集合

  };

  template<class T>

  const int Modify<T>::MaxByte;

  下面逐一解释:

  

  

template<class T>,

  AddressType,MaxByte;

  同上述

  

  

struct ModifyInfoUnit;

  修改要素――地址,新的数值,新数值的字节数。

  

  比较原则――为从头到尾排序提供准则。

  

  输入函数――从输入流中读取一个单元。

  

  

set<ModifyInfoUnit> ModifyInfoSet;

  修改要素的集合,按照地址从小到大自动排序,排序原则由ModifyInfoUnit 的bool operator<>提供。

  

  

template<class T>

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

  {

   InputIsOk=true;

   Input();

  }

  构造函数,设置输入状态。

  

  

template<class T>

  void Modify<T>::Input()

  {

   string fn;

   cout<<"Source binary file name:";

   CIN>>fn;

   SourceFile.open(fn.c_str(),ios::in ios::binary);

   if(!SourceFile)

   {

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

";

  InputIsOk=false;

  return;

   }

   cout<<"Save file name:";

   CIN>>fn;

   DestFile.open(fn.c_str(),ios::out ios::binary);

   if(!DestFile)

   {

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

";

  InputIsOk=false;

  return;

   }

   cout<<"Any character which makes format error will end the input progress.(etc a)

";

   cout<<"Address(Dec)NewValueByteNumber(1--" <<Modify<T>::MaxByte<<")

";

   while(true)

   {

  ModifyInfoUnit MIU;

  //从真实的输入流中读取一个ModifyInfoUnit

  MIU.ReadFrom(CIN.Actual());

  //输入失败就退出循环

  if(!CIN.Actual()) break;

  //把输入成功的单元存入集合,自动排序

  ModifyInfoSet.insert(MIU);

   }

   CIN.ClearAndIgnore();//清除流的失败状态

  }

  用户输入源文件名和目标文件名。有错误的话直接退出。从键盘输入ModifyInfoUnit,输入失败表示停止输入。CIN.Actual()返回真实的流。每次输入一个MIU 就放入集合中自动排序。

  

  set在前面讲了一点,在这里终于现身了。set是一种关联式容器,其自动排序功能需要指定排序准则。对于内部数据类型(比如char 、int、double 等),比较大小的原则是内建的。但是对于用户自定义的类型,就必须指定比较的方法了。set默认的比较法则是“小于”。在ModifyInfoUnit中间提供“小于”的成员函数(bool operator<),就为set使用默认的排序原则提供了定义。

  

  和上面对应,在此也能使用vector,不过使用vector意味着一旦重新分配内存,所有元素都要拷贝,对于int而言,开销可能比较小,但对于这里的ModifyInfoUnit,开销就要多一点了。set只是一种方法,还有一种常用的关联式容器是map。map由要害字key和值value组成。

  

  根据key可以在对数时间内找到对应的value。map是按照key排序的,默认的排序准则是“小于”。假如在这里运用,key就是修改地址,value就是新值及其字节数。

  

  set<ModifyInfoUnit>;就变成map<AddressType,pair<T,int> >;这样就不需要编写比较准则了,其他相应修改就不在这里赘述了,读者可以自己试一下。笔者认为,map和set相比在于能快捷的通过要害字来查找对应值,在这个应用场合还用不到通过地址来查看对应的新值和字节数,故没有采用map。

  

  把ModifyInfoUnit设计为一个全公开的内嵌结构( 或者类), 作为一个私有成员也是常用的手法。

  

  ModifyInfoUnit对外而言是无需了解(私有成员),但是可以被Modify毫无限制的存取。

  

  

  

template<class T>

  void Modify<T>::Run() const

  {

   if(InputIsOk==false) return;

   set<ModifyInfoUnit>::const_iterator Beg=

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

   AddressType Address=0;

   char ch;

   while(SourceFile && Beg!=End)

   {

  //SourceFile没有读完并且集合也没有遍历结束

  if(Address==Beg->Address)

  {

   //到了指定的地址了

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

   for(int k=0;k<Beg->ByteNumber;++k)

   {

  //忽略源文件

  SourceFile.read(&ch,CharSize);

   }

   //写新值

   DestFile.write(P,CharSize*Beg->ByteNumber);

   Address+=Beg->ByteNumber;

   ++Beg;

  }

  else

  {

   SourceFile.read(&ch,CharSize);

   DestFile.write(&ch,CharSize);

   ++Address;

  }

   }

   //源文件中可能还有剩余的内容

   while(SourceFile.read(&ch,CharSize))DestFile.write(&ch,CharSize);

  }

  修改的核心函数。遍历源文件, 当地址等于ModifyInfoSet集合当前元素的地址时,忽略源文件,把新值写入目标文件。一旦源文件读取到了末尾或者集合全部走过了,就跳出循环。

  

  这里的const_iterator,begin和end与前面讲述的作用相似,只是本来是指向链表的,现在指向集合了。

  

  

template<class T>

  bool Modify<T>::ModifyInfoUnit::operator<(const Modify<T>::

  ModifyInfoUnit& rhs) const

  {

   return Address<rhs.Address;

  }

  定义排序原则,应该是一个const成员函数。

  

  

template<class T>

  void Modify<T>::ModifyInfoUnit::ReadFrom(istream& IS)

  {

   IS>>Address>>NewValue>>ByteNumber;

   if(ByteNumber<1ByteNumber>Modify<T>::MaxByte)

  ByteNumber=Modify<T>::MaxByte;

  }

  从输入流读取一个ModifyInfoUnit,假如ByteNumber不符合就修正。手工批量修改的代码也差不多了。这样,对于若干个已知的地址假如要一次性修改,就用这个吧!方便又省事。

实战《仙剑3 》

  

  看到这里也不轻易了,头昏脑胀了吧?放松一下,来试试看我们的工具吧!实验对象是《仙剑3》。选择《仙剑3》是因为笔者正巧在玩这个游戏,虽然网上有《仙剑3》存档的修改器,不过我还是授人以渔吧。

  

  修改存档前要做好备份。所有文件名中间不能有空格等空白符号。游戏第一个存档是pal00.arc,第二个存档是pal01.arc,以此类推。

  

  选择两个文档比如pal01.arc和pal02.arc,都是景天、雪见、龙葵、紫萱四人的组合。记录下两个文档的如下数据:金钱,每个人的经验值,都是4字节。再记录下其中一个存档(比如pal02.arc)中的每个人的“精”、“气”、“神”的上限(4字节),“武”、“防”、“速”和“运” (字节数尚没确定)。注重:“武”、“防”、“速”和“运” 应该是没有使用武器和道具时候的数据。

  

  对于每一个属性,比如景天的经验值,在pal01.arc和pal02.arc 中用CheckBinaryFile找到地址,分别保存为a.txt和b.txt。再用取得交集的方法得到c.txt。那么c.txt的内容就是景天的经验值在存档中的地址,一般是1-2个。

  

  得到金钱和四个人的经验值地址后,用手工批量处理的方法得到新的存档文件,其中金钱可修改的大一点,四个人的经验值不必过大,保证可以升级即可。我得到的数据是:

  

  

  把新的文档覆盖原文档,载入游戏后看看是否正确,然后打一仗后四个人全部升级。存档为pal03.arc,再次记录下每个人的“精”、“气”、“神”的上限,“武”、“防”、“速”和“运”。

  

  由于pal02.arc 和pal03.arc之间存在升级,“精”、“气”、“神”的上限,“武”、“防”都会变化,“速”和“运” 倒是没有变,只能暂时放弃。

  

  故技重演,需要通过pal02.arc和pal03.arc找到四个人的“精”、“气”、“神”的上限,“武”、“防”。于是先得到如下数据:

  

  

  检测到这里,有两个发现:

  

  A. “武”、“防”相差4个地址,那么极有可能:“速”的地址是“防”的地址+4,“运”是“速”+4,而且“武”、“防”、“速”和“运”都是4 字节。4 字节意味着我可以修改为百万,上亿甚至更大的数值。

  

  B. 各属性的地址相对固定。比如经验值地址减去92就是“武”的地址,减去104 就是“精”的地址。这也是合情合理的,笔者猜测该游戏中每个人的属性是一个结构(struct),假如四个人都采用相同的结构,那么每个人的属性之间的地址差都是一样的。

  

  有了上述推算,可以直接计算出剩下的地址了。从经验值地址得到其他属性的地址,笔者懒得自己用计算器重复劳动了,用Excel写个模板,填入经验值地址就能得到其他地址了。于是得到如左下表格所示的结果:

  

  

  

  由于要修改的东西比较多,以后可能还要用到这些数据,我们可以把要修改的指令保存到一个文本文件(比如haha.txt),用输入重定向的方法执行程序。该文本文件的内容就是使用该工具时,从键盘输入的数据。为节省篇幅,我只列出修改金钱和男主角属性的内容:

  

  

2

  pal15.arc

  pal15.bak

  148 987654321 4

  8929 987654321 4

  1456 3000000 4

  1364 1000001 4

  1368 1000002 4

  1372 1000003 4

  1376 1000004 4

  1352 1000005 4

  1640 1000005 4

  1356 1000007 4

  1644 1000007 4

  1360 1000009 4

  1648 1000009 4 X

  -1

  h

  执行过程由于采用了输入重定向,所以本来通过键盘输入的数据现在从haha.txt读取:

  

  

  覆盖原文档,载入游戏,战斗胜利后触发升级。给几张截图看看:

  

  

  对于游戏中的龙精石,我还没有找到修改方法,或许不能修改。根据我自己的实践,“速”和“运”不要修改的太高(一两百就够了),或者干脆不要改了,否则会导致比较希奇的问题。当游戏中人物不同时候,存放地址可能不同,不过各个人物的属性的相对地址还是一样的。

  

  经验值不必改得太高,只要可以触发战斗后的升级即可,一旦到了99级就不能升级了。对于每个人使用魔法的次数也能修改的,比如景天第一次使用“风咒”存盘一下,再使用一次再存盘。检查两个存盘文件的数值1和2,再用取得交集的方法得到一个唯一的地址,然后手动批量修改一下。我在网上看到说魔法次数最大显示为255,因此修改成2字节的200就够了。载入游戏再运用一次,就会变成级别4。像景天现在的“武”超过了一百万,攻击力起码几十万,大概没有人可以反抗了。

  

  对于好感度,战斗中(景天处于阵中)可以通过援助来增加好感度、女战友死亡会降低好感度,对话会影响好感度(比如在网上看到对话结束后的长音代表好感度+5,中音+3)。网上有些功略指出了对话的好感度增加值。我是这样试验的:选一个包含全部女主角的组合存档(比

  如pal01.arc),修改她们的生命值到比较低的值(比如1), 战斗中让她们全部死亡,唯独自己活着,然后存档(pal02.arc)。我估计死亡一次大概降低2个好感度,于是pal01.arc与pal02.arc的相对值为2,字节数也为2,运行程序找到好多个地址,打开一看,前面几行是:

  

  

Relative value is 2

  Address Value1 Value2

  696 45 43

  700 34 32

  704 31 29

  15962 2 0

  19341 2 0

  估计这三个地址是696,700和704,于是批量修改为80、85、90,下面是一张截图:

  

  

  这个游戏笔者第一次只玩到锁妖塔,觉得迷宫太复杂了没有继续下去,后来写了这个程序后又重新开头试了一下,AI进攻真是干脆利落,目前发现存在两个问题:

  

  ①.假如修改某人的“精气神”的上限,后来她离开队伍又加入了,这时“精气神”上限只有几百,这只出现在离开队伍后来又加入的情况。

  

  ②.在拿到土灵珠被大胡子抢走后,剧情要求战败的。我当时修改后四个人全部是AI进攻,每个人的“武”都超过了一百万,硬是打不死

  大胡子,但我每个人的“精”、“防”也都很高,这样就造成了死循环,只能Ctrl+Alt+Del强制退出,看来只能把当前的“精”修改到最小才能战败。

  

  对于数值明确的对象,通过相对值查找也是一种方法,非凡是游戏初期,数值比较小,内容相同的地址很多,既可以通过“取交集”,也能使用“相对值”。

  

  总结

  

  修改游戏主要分为两种:动态和静态。本文介绍的是静态法,在游戏存档中目的明确的修改;动态法就是直接在游戏过程中修改内存数据了,还能进行一些模糊查找,两者各有特点。笔者孤陋寡闻,没发现比较便利的修改游戏存档的工具,就自己写了一个(版权所有,哈哈)。程序主要是模拟UE手工修改游戏存档的原理来制作的,并陆续加入了一些自己想到的功能。一旦游戏对存档文件进行了加密或者校验就无能为力了(UE也应该如此吧!)。

  

  程序的顺利完工很大程度上得益于C++的STL,比如笔者运用了vector、list、set 等容器,sort、unique、copy和set_itersection等算法以及一些常用迭代器。使用STL的好处就是安全、高效、简洁。编译器选择了Borland 的C++ Builder 6,这是Windows平台中对标准C++支持较好的一种。

  

  笔者的C++是工作之余自学的,只有两年左右时间,这个程序是一时心血来潮编写的,不足之处肯定是有的。用编程来修改游戏算是学以致用,寓教于乐,还可以给自己玩游戏找借口。初衷是给自己用用,不怕丢脸,后来觉得还有点技术含量,就写了这篇文章,希望对C++初学者有点帮助,给玩游戏的带来点快乐。

  

  

精彩图集

赞助商链接