首页 > 学院 > 开发设计 > 正文

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

2019-11-17 05:35:13
字体:
来源:转载
供稿:网友

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

  改进2:相对值查找

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

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

template<class T>
void RelativeValue()
{
 typedef fstream::off_type AddressType;
 EInputStream CIN(cin);
 string FN;
 cout<<"First binary file name:/t";
 CIN>>FN;
 ifstream Read1(FN.c_str(),ios::in ios::binary);
 //读+二进制模式
 if(!Read1)
 {
  cerr<<"Open "<<FN<<" failed./n";
  return;
 }
 cout<<"Second binary file name:/t";
 CIN>>FN;
 ifstream Read2(FN.c_str(),ios::in ios::binary);
 if(!Read2)
 {
  cerr<<"Open "<<FN<<" failed./n";
  return;
 }
 int ByteNumber;
 cout<<"Byte number:/t";
 CIN>>ByteNumber;//字节数
 int RV;//指定的相对值
 cout<<"Relative value(value1-value2):/t";
 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./n";
  if(Occurs==0) return;
  cout<<"Input save filename:/t";
  CIN>>FN;
  //保存至文件
  ofstream SaveFile(FN.c_str());
  if(!SaveFile)
  {
   cerr<<"Create "<<FN<<" failed./n";
   return;
  }
  SaveFile<<"Relative value is " <<RV<<"/nAddress/tValue1/tValue2/n";
  for(InfoList::const_iterator Beg=IL.begin(), End=IL.end();Beg!=End;++Beg)
  {
   SaveFile<<(*Beg).first<<’/t’ <<(*Beg).second.first <<’/t’<<(*Beg).second.second <<’/n’;
  }
 }
  这里的新面孔是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:/t";
 CIN>>fn;
 SourceFile.open(fn.c_str(),ios::in ios::binary);
 if(!SourceFile)
 {
  cerr<<"Open "<<fn<<" failed./n";
  InputIsOk=false;
  return;
 }
 cout<<"Save file name:/t";
 CIN>>fn;
 DestFile.open(fn.c_str(),ios::out ios::binary);
 if(!DestFile)
 {
  cerr<<"Create "<<fn<<" failed./n";
  InputIsOk=false;
  return;
 }
 cout<<"Any character which makes format error will end the input progress.(etc a)/n";
 cout<<"Address(Dec)/tNewValue/tByteNumber(1--" <<Modify<T>::MaxByte<<")/n";
 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个。

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

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

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

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

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

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

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

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

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

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


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

  由于要修改的东西比较多,以后可能还要用到这些数据,我们可以把要修改的指令保存到一个文本文件(比如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读取:

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

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

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

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

  经验值不必改得太高,只要可以触发战斗后的升级即可,一旦到了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,下面是一张截图:

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

  这个游戏笔者第一次只玩到锁妖塔,觉得迷宫太复杂了没有继续下去,后来写了这个程序后又重新开头试了一下,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++初学者有点帮助,给玩游戏的带来点快乐。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表