| 導購 | 订阅 | 在线投稿
分享
 
 
 

用C++制作自己的遊戲修改器(上)

來源:互聯網網民  2008-06-01 01:57:09  評論

本文旨在說明修改遊戲存檔的思路、編程方法和一點技巧,並無其他不良企圖。假如僅僅爲了修改遊戲,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("test.txt",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);//重新指向文件開頭預備讀取

BinFile.read(reinterpret_cast<char*>(&i),sizeof(int));

cout<<"i="<<i<<』\n』;

}

用UE打開test.txt切換到二進制模式,是這樣子的:

在計算器中看到的是04D2,在UE 中看到的是D204,這就是筆者所謂的不直觀性。因此,假如你要在某個遊戲存檔文件中間(擴充開來就是二進制文件)尋找04D2這個數值,找到上圖顯示的地方就對了。筆者初期手工修改存檔也是這樣的,比較麻煩。

下面這個小程序表明了模擬UE在二進制文件中尋

找整數的原理:

#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_cast<const char*>(&i),

sizeof(int));//強制轉換,把i用二進制方式寫入文件

BinFile.seekg(0,ios::beg);

//重新指向文件開頭,預備讀取

char ch;

while(BinFile.read(&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++制作自己的遊戲修改器(上)
更多內容請看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:\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);

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/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.\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(),AddressList.end(),

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

}

把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.\n";

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.\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++制作自己的遊戲修改器(上)
更多內容請看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.\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;

}

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<<".\n";

return;

}

cout<<V3.size()<<" value were enumerated.\n";

cout<<"Input save filename:\t";

CIN>>fn;

ofstream Dest(fn.c_str());

if(!Dest){

cerr<<"Create "<<fn<<" failed.\n";

}

else{

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

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/C++技術專題 網絡遊戲攻略 遊戲策劃專題,或

 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
  本文旨在說明修改遊戲存檔的思路、編程方法和一點技巧,並無其他不良企圖。假如僅僅爲了修改遊戲,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("test.txt",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);//重新指向文件開頭預備讀取  BinFile.read(reinterpret_cast<char*>(&i),sizeof(int));  cout<<"i="<<i<<』\n』; }   用UE打開test.txt切換到二進制模式,是這樣子的:   在計算器中看到的是04D2,在UE 中看到的是D204,這就是筆者所謂的不直觀性。因此,假如你要在某個遊戲存檔文件中間(擴充開來就是二進制文件)尋找04D2這個數值,找到上圖顯示的地方就對了。筆者初期手工修改存檔也是這樣的,比較麻煩。   下面這個小程序表明了模擬UE在二進制文件中尋   找整數的原理: #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_cast<const char*>(&i),  sizeof(int));//強制轉換,把i用二進制方式寫入文件  BinFile.seekg(0,ios::beg);  //重新指向文件開頭,預備讀取  char ch;  while(BinFile.read(&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"; } [url=/bbs/detail_1785068.html][img]http://image.wangchao.net.cn/it/1323424833436.gif[/img][/url] 更多內容請看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:\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);  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去掉一個最早讀取的,然後讀入一個新的,繼續匹配。函數返回匹配的個數。 [url=/bbs/detail_1785068.html][img]http://image.wangchao.net.cn/it/1323424833477.gif[/img][/url] 更多內容請看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.\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(),AddressList.end(),  ostream_iterator<T>(os,"\t")); }   把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.\n";    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.\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),我以前不知道蘇媚還有「狐舞動天」的特技,嗬嗬! [url=/bbs/detail_1785068.html][img]http://image.wangchao.net.cn/it/1323424833504.gif[/img][/url] 更多內容請看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.\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;  }  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<<".\n";   return;  }  cout<<V3.size()<<" value were enumerated.\n";  cout<<"Input save filename:\t";  CIN>>fn;  ofstream Dest(fn.c_str());  if(!Dest){   cerr<<"Create "<<fn<<" failed.\n";  }  else{   copy(V3.begin(),V3.end(),ostream_iterator<T>(Dest,"\t"));   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個。知道了真正的地址,對于自動産生的文件就可以有的放矢的選擇了。 (未完待續) [url=/bbs/detail_1785068.html][img]http://image.wangchao.net.cn/it/1323424833536.gif[/img][/url] 更多內容請看C/C++技術專題 網絡遊戲攻略 遊戲策劃專題,或
󰈣󰈤
王朝萬家燈火計劃
期待原創作者加盟
 
 
 
>>返回首頁<<
 
 
 
 
 
 熱帖排行
 
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有