1、用 C+制作自己的游戏修改器(上) 2005-05-30 12:18 作者:方如坤出处:csdn 开发高手责任编辑:方舟本文旨在说明修改游戏存档的思路、编程方法和一点技巧,并无其他不良企图。如果仅仅为了修改游戏,FPE、金山游侠等更为专业。前言大多数程序员都玩过游戏,也或曾想过修改游戏,笔者也不例外。我通常不希望自己受困于游戏中的经验值、金钱之类的,于是采用修改游戏存档文件的方法,自己动手修改比起使用金山游侠等更有乐趣。毕竟有时候只要享受一下游戏的情节就够了,把大量的时间花费在增加经验值、赚钱方面太不合算了,毕竟时间有限而游戏无限!方法嘛,使用老牌的 UltraEdit(以下简称 UE),当然
2、还需要配合“计算器”进行十进制和十六进制的转换。时间长了,也觉得繁琐,何不自己动手写一个针对游戏存档文件的修改器而一劳永逸?笔者比较喜欢 C+,如果你有一定的 C+基础,跟我走吧! 笔者的电脑:AMD XP1700+,Windows2000( sp4),Borland C+ Builder 6(sp4) 手工修改游戏存档文件的方法游戏存档文件大多使用二进制格式,这样对于读取和保存数据都比较方便。可使 用 Windows 的“ 计算器” 来看看 10 进制和 16 进制的区别:采用“ 科学性”模式,在 10 进制模式下输入数据,然后切换到 16 进制就行了。不过就算这样转换,看起来还是不很直观,
3、因为在游戏存档中并不是如此显示的。 那么 用 C+如何表达的呢?下面这个小程序演示了如何读写二进制整数。#include iostream #include fstream using namespace std;/标准库所在的空间int main() fstream BinFile(“test.txt“,ios:in | ios:out | ios:binary);/读+写+二进制模式 int i=1234; BinFile. write(reinterpret_castconst char*(/reinterpret_cast 是 C+的强制转换 ,这里把整数的地址强制转换为 const
4、char*,/ 与 C 的(const char*) BinFile.seekg(0,ios:beg);/重新指向文件开头准备读取 BinFile. read(reinterpret_castchar*( cout“i=“ in; 用 UE 打开 test.txt 切换到二进制模式,是这样子的: 在计算器中看到的是 04D2,在 UE 中看到的是 D204,这就是笔者所谓的不直观性。因此,如果你要在某个游戏存档文件中间(扩充开来就是 二进制文件) 寻找 04D2 这个数值,找到上图显示的地方就对了。笔者初期手工修改存档也是这样的,比较麻烦。下面这个小程序表明了模拟 UE 在二进制文件中寻找整数
5、的原理: #include iostream #include fstream using namespace std; int main() fstream BinFile(“test.txt“,ios:in | ios:out | ios:binary);/读+写+二进制模式const int i=87654; BinFile.write(reinterpret_castconst char*(/强制转换,把 i 用二进制方式写入文件BinFile.seekg(0,ios:beg); / 重新指向文件开头,准备读取char ch; while(BinFile.read(/显示 /stati
6、c_cast 是 C+的静态转换,与 C 的(int)ch 作用相 /同,但是 static_cast 意思表达更清楚。coutn; /下面把 i 的地址转换为字符串地址,并用 char 方式依次读取,主要是比较两者读取的结果是否相同. const char* P=reinterpret_castconst char*( for(int i=0;isizeof(int);+i) coutstatic_castint(Pi) “t“; 自动检查游戏存档中的数值手工在存档文件中使用 UE 中来查找某个数值的时候,可能找到好多地方,靠一个一个查找然后记录下地址可真费眼神。写个程序来自动寻找指定的数值
7、,并且记录下地址吧!本文所述的地址都是从 0 开始的,而且都以十进制方式输入输出。templateclass 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; mutabl
8、e bool InputIsOk; mutable ifstream BinaryFile; mutable listAddressType AddressList; void Input(); int Check() const; void SaveAddressToFile(ostream void AutoModifySave(const T ; templateclass T const int CheckBinaryFileT:MaxByte;/定义静态整型常量这是自己定义的一个类,下面逐一解释: templateclass T T 代表要寻找的数据的类型。当然,这个程序只是寻找整数
9、(经验值、金钱都是整数!),但我不排除以后要查找其他类型的数据。为了可扩充性,使用了模板。typedef fstream:off_type AddressType; 我要找到数据在文件中总有地址,这个地址是什么类型呢? int 还是 long,或者是其他类型?fstream 有一个类型叫 off_type,应该是偏移类型的含义,在这里我把这个类型叫做 AddressType。static const int MaxByte=sizeof(T); 这是一个静态整型常量,表示 T 的大小(最多有多少字节) ,比如在我的机器上,sizeof(int)=4。T 的大小在编译的时候就确定,而且它不能被修
10、改(const),对于所有查找类型相同的 CheckBinaryFile,这个数值是唯一的,共享的(static)。构造函数: templateclass T CheckBinaryFileT :CheckBinaryFile():CharSize(sizeof (char),CIN(cin) InputIsOk=true; Input(); CharSize 为 sizeof(char),把 cin 绑定到 CIN。由于 CharSize 是常量,必须在构造函数的初始化列表中设定。预设输入状态,调用输入函数: templateclass T void CheckBinaryFileT:Inp
11、ut() cout“Binary file name:t“; CINFileName; 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“; CINOldData; cout“Byte number(1-“CheckBinaryFileT:MaxByte“):t“; CINByteNumber; if(ByteNumber1 |
12、 ByteNumberCheckBinaryFileT :MaxByte) /字节数错误,调整为最大值 ByteNumber=CheckBinaryFileT:MaxByte; cout“Byte number was amended to “ CheckBinaryFileT:ByteNumber n; 提示用户输入二进制存档文件,用只读+二进制模式开启。如果失败,设置输入状态为 false,直接退出。然后提示用户输入要查找的整数(OldData)以及多少个字节(ByteNumber)。如果字节数错误,调整为最大值。由于计算机系统的不同以及 char,short,int,long 之间存在转
13、换关系,对于某些整型的字节数是不可确定的。比如 100,可以用 char 表示,那么只需要 sizeof(char)个字节表示就够了,当然也可以用字节数更多的类型,比如 int,来表示 100。templateclass T int CheckBinaryFileT:Check() const const char* P=reinterpret_castconst char*( char RangeCheckBinaryFileT:MaxByte; int Occurs=0; AddressType Addr=0; / 填充 0 memset(Range,0,CheckBinaryFileT:
14、MaxByte*CharSize); BinaryFile.read(Range,CharSize*ByteNumber);/填满 Range while(BinaryFile) if(memcmp(P,Range,CharSize*ByteNumber)=0)/匹配成功 AddressList.push_back(Addr); +Occurs; /删除一个最旧的 memcpy(Range, /读入一个新的 BinaryFile.read( +Addr; return Occurs; 检查输入的二进制文件中有多少个 OldData,并保存地址,用模拟二进制方式比较 OldData。Range
15、是一个比较区域,这里不打算输出这个字符串,也不考虑用 strcpy 来拷贝内容,所以不必预留一个空间来保存结尾符号0。填满 Range 后,开始一个一个字符比较了:当 Range 和 OldData 完全相同就表示匹配成功(memcmp 返回 0 表示成功) ,一旦成功,就把该地址保存下来(AddressList)。不管是否成功,把 Range 去掉一个最早读取的,然后读入一个新的,继续匹配。函数返回匹配的个数。list 是标准 C+的一个容器,类似双向链表,在添加/删除节点方面表现优秀。我不打算使用排序,因为从头到尾遍历文件时保存下来的地址肯定是有序的;我也不需要随机读取这些地址,所以排除了
16、 vector以及 deque 这两种容器。至于没有采用内建的数组,咳,我不知道能找到多少地址,或许一个都没有,或许成千上万。list 有一个 size()函数,望文生义就是大小的意思,的确如此。不过由于 list 是一种链表,不像数组那样只要把头尾指针相减就能得到大小,取得 size 的办法只有从头到尾走一遍,速度比较慢。既然这个函数很清楚取得了多少个地址,那就直接返回这个数目吧! templateclass T void CheckBinaryFileT:Run() if(InputIsOk=false) return; const int Occurs=Check(); coutOccu
17、rs“ different addresses were found.n“; if(Occurs=0) return; cout“Save address info to files(y/n)?t“; char YN; CINYN; if(YN=y | YN=Y) cout“Address file name:t“; string AddressFileName; CINAddressFileName; ofstream Save(AddressFileName.c_str(),ios:out); if(!Save) cerr“Create “AddressFileName “ failed.
18、n“; else SaveAddressToFile(Save); Save.close(); cout“Modify binary file automatically(y/n)?t“; CINYN; if(YN=y | YN=Y) cout“New value:t“; T NewValue; CINNewValue; system(“dir tmp“); system(“del */q“); AutoModifySave(NewValue); 如果输入错误,则直接退出。显示匹配的个数并询问是否保存这些地址至文件。再询问是否自动修改。比如找到了 10 个地址,自动修改将产生 10 个新文件,
19、每个文件与原文件相比都只修改了一个地址的数值。输入新的数值,将产生若干个新文件。新文件的格式是+地址的十进制表示。产生新文件前先把旧的以开头的文件删除。如果不存在开头的文件,system(“del */q“);会说找不到文件,不大舒服,那我先制造一个tmp(system(“dir tmp“);),这里使用了 DOS 的输出重定向,把原本显示到屏幕的内容输入到tmp 中。templateclass T void CheckBinaryFileT:SaveAddressToFile(ostream 把 AddressList 的内容保存下来。copy 是 C+的函数,把一个区间的内容拷贝到另一个地
20、方。templateclass T void CheckBinaryFileT:AutoModifySave(const T const char* P=reinterpret_castconst char*( 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
21、Write(NewFileName.c_str(),ios:out | ios: binary); if(!Write) cerrNewFileName“ . unsuccessfully.n“; continue; while(Addr *Beg Write.write( +Addr; for(int k=0;kByteNumber;+k)/忽略源文件BinaryFile.read( Write.write(P,CharSize*ByteNumber); /写入新值while(BinaryFile)/源文件剩余的内容拷贝到新文件BinaryFile.read( Write.write( Wr
22、ite.close(); coutNewFileName“ . successfully.n“; /for 根据 AddressList 的大小遍历若干遍源文件。新的文件用+地址格式。先把小于指定地址的内容拷贝到新文件,到了指定地址后把新值写入新文件,再把源文件剩余的内容拷贝到新文件。const_iterator是常量迭代器,表明不修改 AddressList 的内容。begin 函数得到 AddressList 的开头,end 函数得到AddressList 的最后一个元素的下一个地址,+ 表示迭代器前进一格。把源文件剩余的内容拷贝到新文件后,会导致源文件 BinaryFile 的状态为 b
23、ad,在 bad 状态下要执行比如读写、重新指向文件某个位置等操作必须先调用 clear 清除这个状态。mutable 是 C+新近的关键字,大体意思是表明该内容可以在 const 成员函数中修改。比如在这个类中间,比如 mutable bool InputIsOk;InputIsOk 只是表明用户输入数据的正确性,并不影响自身的状态; mutable listAddressType AddressList;也没有改动源文件的各个属性,只是保存了信息。好了,这个类基本写完了。他的功能是: 输入一个二进制文件名以及要查找的整数和字节数。告诉你找到了多少个地址(可保存地址信息到文件 ),如果你愿意
24、,可以分别把这些地址上的数据修改为新的数值后产生新文件。你可以在仙剑 2 上做实验。仙剑 2 的存档地址不是固定的。记录下当前的经验值和金钱( 都是 4 字节),存档后切换到 Windows,对存档的文件开刀,如果报告找到的地址只有四五个,可以自动产生新文件。把新文件覆盖原存档,切换到游戏后读取刚刚修改的文件试试看。大不了直接退出游戏。仙剑 2 可以直接切换到 Windows,这对于修改存档比较方便。我以前老老实实玩到底才 32 级,现在可以一下子飙升到七八十级(最高好像是 99),我以前不知道苏媚还有“狐舞动天”的绝技,嗬嗬!改进 1 :对地址文件取得交集应该说有些游戏的存档还是很老实的地址
25、不变。对于这种类型的存档,我们可以用对集合取交集的方法来缩小范围。比如经验值为 4 的时候存档为A,经验值为 7 的时候存档为 B。对 A 用上面的工具查找 4,保存地址信息为 4.txt;对 B 用上面的工具查找 7,保存地址信息为 7.txt。把 4.txt 和 7.txt 的内容看作两个集合,如果地 址不变,那么取得两者的交集就能大大缩小查找范围。嗯,仙剑 2 不行,仙剑 1 和 3 倒是可以的。对于集合的个数,至少两个,可以对多个集合取交集。C+提供了 set_intersection 函数,可以对两个有序区间进行交集运算,我们只需要不断重复这个过程,就能对多个集合执行交集运算了。约定
26、:输入若干个集合文件进行交集元算,当输入一个不存在的文件表示结束输入。当程序发现取得空集的时候就自动结束。templateclass T void GetIntersection() EInputStream CIN(cin); cout“Input some text filenames for reading,end with a nonexistent one.n“; string fn; CINfn; ifstream Read(fn.c_str(); if(!Read) cerr“Open “fn “ failed.n“; return; vectorT V1; copy(istre
27、am_iteratorT(Read),istream_iteratorT (),back_inserter(V1);/保存 file1 的内容到 V1 CINfn; Read.clear(); Read.close(); Read.open(fn.c_str(); if(!Read) cerr“Open “fn “ failed.n“; return; vectorT V2,V3; copy(istream_iteratorT(Read),istream_iteratorT (),back_inserter(V2);/保存 file2 的内容到 V2 sort(V1.begin(),V1.en
28、d();/排序/ 删除重复的数据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) /如果是空集就可以退出了 CINfn; Read.clear(); Read.close(); Rea
29、d.open(fn.c_str(); if(!Read) break; vectorT().swap(V1);/清除 V1 copy(istream_iteratorT(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 交换 vectorT().swap(V3);/清除 V3 set_intersection(V1.begin(),V1.end(), V2.begi
30、n(),V2.end(),back_inserter(V3); if(V3.empty() cout“An empty aggregate was found after reading “ fn“.n“; return; coutV3.size()“ value were enumerated.n“; cout“Input save filename:t“; CINfn; ofstream Dest(fn.c_str(); if(!Dest) cerr“Create “fn“ failed.n“; else copy(V3.begin(),V3.end(),ostream_iteratorT
31、(Dest,“t“); Dest.close(); 下面逐一解释: templateclass T 和上一例含义一样,在此代表集合元素的类别。我可以对整数集合进行交集元算,对小数、字符串组成的集合也能进行交集元算。当然我现在只用到了整数集合。CIN 是我自己的一个加强类,你可以看作 cin。首先打开两个指定的文件(做交集运算至少要两个集合 ),如果有一个失败就退出。把这两个文件的内容分别放入 V1 和 V2。然后对 V1 和 V2 排序(sort) ,剔除重复内容(unique 和 erase)。对调整过的 V1 和 V2 执行交集,结果保存到 V3。当 V3 不为空集的时候开始循环:读取下一
32、个等待输入的文件。清空 V1,把新的文件内容放入 V1,把 V3 的内容拷贝到 V2,清空 V3,把 V1 和 V2 的交集放入 V3。上述“把 V3 的内容拷贝到 V2”只是表达一个意思,实际上只是把 V3 和 V2 做交换而已,因为 V3 我需要清空,并不需要真正的拷贝。把某个集合清空,只是和临时的空集做交换而已。这里我使用 vector 容器,set 也是可以的。使用 set 的好处是可以自动排序和剔除重复内容,当然自动排序和保持元素的唯一性是需要代价的。使用 vector 的好处是等到所有输入完毕后,执行某些函数(比如 sort,unique,erase)来完成上述功能,一次性达到目的
33、,而不像 set 那样任何时刻都保持元素的有序性和唯一性。当数据量比较大的时候,vector 或许要高效一些。当然,主观臆断不是科学精神,实践是最好的检验手段。我在这里只是随便选取了 vector。一旦选择了 vector,那么“清除所有内容”最好使用“ 与空的临时vector 交换”,采用这种方法后,vector 的容量也会变得尽可能的小;而如果采用 clear 的方法,容量保持不变。因为 vector 内部也采用数组,数组就意味着一块连续的内存,一旦需求超出了容量会导致重新分配,所以 vector 会采用预留一部分空间的策略,避免每次增加元素都要重新分配。而 set 不一样,底层采用二叉树
34、(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+ 的全部文章