DirectX 9 遊戲漢化詳解

1 舊 默認 【原創】DirectX 9 遊戲漢化詳解
noword_forever 當前離線

【文章標題】: DirectX 9 遊戲漢化詳解
【文章作者】: noword
【軟件名稱】: 無厘頭太空戰役
【下載地址】: http://www.verycd.com/topics/2819995/
--------------------------------------------------------------------------------
  【前言】
  先copy一段此遊戲介紹:
  
  這是一個獨特的戰略遊戲,具有即時戰略與塔防的混合風格,玩家將扮演龐大太空艦隊的最高指揮官,你可以自定飛船的構造,擺放飛船的位置,下達命令,然後觀看絢麗的射擊與爆炸。移動和爆炸時會有動態模糊效果。支持自定義地圖。
  
  想玩中文版,兩個遊戲論壇,3DM和YX上,都有人說要漢化,等了幾個月,沒有下文,說是技術原因。於是決定自己來試試看。
  
  
  【困難何在】
  此遊戲的文本都在data目錄下,都是明文的文本文件。修改data\strings.ini,將
  
代碼:
  MAINMENU_QUIT        = "Exit"
  
  改成
  
代碼:
  MAINMENU_QUIT        = "退出"
  
  
  進入遊戲後,不出所料,無法顯示此中文。
  
  茫茫多的遊戲愛好者,在嘗試漢化某款自己心儀的遊戲時,都是死在了這一步——找了半天文本資源,然後翻成中文,滿心歡喜和期待的進入遊戲,面對的卻是一堆亂碼或一片空白。滿腔熱情,化爲烏有,無可奈何,黯然神傷。
  
  本文的目的,就是希望能夠幫助這些有志於遊戲漢化的同學,主要介紹瞭如何讓一個英文的遊戲,能夠正確的顯示出中文。
  
  
  【調試分析】
  DirectX 9遊戲的啓動流程是這樣的,先執行Direct3DCreate9,返回值是一個IDirect3D9句柄,然後執行IDirect3D9->CreateDevice,得到IDirect3DDevice9句柄。
  有了IDirect3DDevice9就能使用DirectX 9的一切繪圖手段,而我們最關心的就是能夠使用D3DXCreateFont來創建ID3DXFont,繼而能夠非常方便快捷的在遊戲中顯示文字。
  
  用ODBG載入遊戲的exe文件,“查找所有模塊間的調用”,找“d3d9.Direct3DCreate9”:
  
代碼:
  00501974   .  BB 500A5400        mov     ebx, 00540A50                   ;  ASCII "Initialising 3D Engine"
  00501979   .  E8 22130000        call    00502CA0
  0050197E   .  6A 20              push    20
  00501980   .  8977 18            mov     dword ptr [edi+18], esi
  00501983   .  E8 2E8B0100        call    <jmp.&d3d9.Direct3DCreate9>
  00501988   .  85C0               test    eax, eax
  0050198A   .  8947 10            mov     dword ptr [edi+10], eax         ;  edi+10 = 58d550
  
  往下找,就能找到IDirect3D9->CreateDevice:
  
代碼:
  00501B4C   .  8D77 14            lea     esi, dword ptr [edi+14]
  00501B4F   .  56                 push    esi                              ;  58d554  => IDirect3DDevice9
  00501B50   .  8D4F 40            lea     ecx, dword ptr [edi+40]
  00501B53   .  51                 push    ecx
  00501B54   .  6A 40              push    40
  00501B56   .  EB 14              jmp     short 00501B6C
  ...
  00501B6C   >  8B4F 18            mov     ecx, dword ptr [edi+18]
  00501B6F   .  8B47 10            mov     eax, dword ptr [edi+10]
  00501B72   .  8B10               mov     edx, dword ptr [eax]
  00501B74   .  8B52 40            mov     edx, dword ptr [edx+40]
  00501B77   .  51                 push    ecx
  00501B78   .  8B4C24 24          mov     ecx, dword ptr [esp+24]
  00501B7C   .  51                 push    ecx
  00501B7D   .  55                 push    ebp
  00501B7E   .  50                 push    eax
  00501B7F   .  FFD2               call    edx                             ;  IDirect3D9::CreateDevice
  
  
  由於是所謂的COM接口,沒有十分明顯的標誌,不是很好找。
  
  在微軟的DirectX SDK d3d9.h文件中,IDirect3D9的接口是這樣的:
  
  
代碼:
  DECLARE_INTERFACE_(IDirect3D9, IUnknown)
  {
      /*** IUnknown methods ***/
      STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObj) PURE;
      STDMETHOD_(ULONG,AddRef)(THIS) PURE;
      STDMETHOD_(ULONG,Release)(THIS) PURE;
  
      /*** IDirect3D9 methods ***/
      STDMETHOD(RegisterSoftwareDevice)(THIS_ void* pInitializeFunction) PURE;
      STDMETHOD_(UINT, GetAdapterCount)(THIS) PURE;
      STDMETHOD(GetAdapterIdentifier)(THIS_ UINT Adapter,DWORD Flags,D3DADAPTER_IDENTIFIER9* pIdentifier) PURE;
      STDMETHOD_(UINT, GetAdapterModeCount)(THIS_ UINT Adapter,D3DFORMAT Format) PURE;
      STDMETHOD(EnumAdapterModes)(THIS_ UINT Adapter,D3DFORMAT Format,UINT Mode,D3DDISPLAYMODE* pMode) PURE;
      STDMETHOD(GetAdapterDisplayMode)(THIS_ UINT Adapter,D3DDISPLAYMODE* pMode) PURE;
      STDMETHOD(CheckDeviceType)(THIS_ UINT Adapter,D3DDEVTYPE DevType,D3DFORMAT AdapterFormat,D3DFORMAT BackBufferFormat,BOOL bWindowed) PURE;
      STDMETHOD(CheckDeviceFormat)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,DWORD Usage,D3DRESOURCETYPE RType,D3DFORMAT CheckFormat) PURE;
      STDMETHOD(CheckDeviceMultiSampleType)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SurfaceFormat,BOOL Windowed,D3DMULTISAMPLE_TYPE MultiSampleType,DWORD* pQualityLevels) PURE;
      STDMETHOD(CheckDepthStencilMatch)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,D3DFORMAT RenderTargetFormat,D3DFORMAT DepthStencilFormat) PURE;
      STDMETHOD(CheckDeviceFormatConversion)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SourceFormat,D3DFORMAT TargetFormat) PURE;
      STDMETHOD(GetDeviceCaps)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DCAPS9* pCaps) PURE;
      STDMETHOD_(HMONITOR, GetAdapterMonitor)(THIS_ UINT Adapter) PURE;
      STDMETHOD(CreateDevice)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,HWND hFocusWindow,DWORD BehaviorFlags,D3DPRESENT_PARAMETERS* pPresentationParameters,IDirect3DDevice9** ppReturnedDeviceInterface) PURE;
      
      #ifdef D3D_DEBUG_INFO
      LPCWSTR Version;
      #endif
  };
  
  
  CreateDevice是第17個函數,所以它的地址是(17-1)*4 = 0x40,
  00501B74   .  8B52 40            mov     edx, dword ptr [edx+40]
  這裏的edx+40就是這麼來的。
  
  如果覺得算起來很麻煩的話,還有一個簡單的方法,可以自己編譯一個d3d9的程序,然後反彙編看看。
  
  如果還覺得麻煩,還有個更簡單的方法,直接往下找,通常會有一些調試文本能夠幫助定位,例如:
  00501B86   .  BB 600B5400        mov     ebx, 00540B60                   ;  ASCII "CreateDevice"
  ...
  00501BBB   .  BB 700B5400        mov     ebx, 00540B70                   ;  ASCII "CreateDevice failed again"
  當然,這個辦法並不通用,有效與否完全要看遊戲作者的臉色。
  
  知道了IDirect3DDevice9的地址,就能植入我們自己的初始化ID3DXFont的代碼。
  
  有了ID3DXFont,下面就是要找到用於顯示文字的函數,並用我們自己的代碼來替換實現。
  
  隨便找一段遊戲中出現的文字,比如開始菜單上出現的“Full 1.37”,ALT-M,進入內存窗口,在所有搜到的該字符串上下“內存訪問”斷點,最終會找到地址在4FFF50的一個函數。
  
  該函數返回時,用的是“retn 14”,在4FFF50處用“retn 14”改寫,切回到遊戲後沒有任何字符出現,說明這個函數正是用來顯示字符的。
  
  原諒我在這裏對於怎麼找到4FFF50的過程含糊其辭,一筆帶過了。確實沒有什麼取巧的方法,完全取決於破解的功力,良好的耐力,以及一點點好運氣。而這也正是遊戲漢化的難點所在。
  
  找到字符串顯示的函數後,還要弄清楚該函數的接口。
  
  在屏幕上顯示一個字符串,通常需要知道這些要素:字符串、顯示的位置(X,Y座標)、顏色、字符的大小,以及一些flag用於表示左對齊,右對齊,居中,加粗,傾斜等。
  
  前面說過,返回用的是“retn 14”,說明棧裏有20(10進制的14h)/4=5個參數,當進入該函數時,棧上的數據是這樣的:
  
代碼:
  0012FE84   00453C48  返回到 GSB_1_37.00453C48 來自 GSB_1_37.004FFF50
  0012FE88   0012FEB8  ASCII "Full 1.37"
  0012FE8C   00000000
  0012FE90   40400000
  0012FE94   FFFFFFFF
  0012FE98   447D8000
  
  第一個,很明顯就是要顯示的字符串,後面幾個是什麼玩意兒呢?
  
  回到調用004FFF50的地方,在00453C1D下斷點:
  
代碼:
  00453C1D  |> \D94424 14          fld     dword ptr [esp+14]
  00453C21  |.  51                 push    ecx
  00453C22  |.  D91C24             fstp    dword ptr [esp]                  ;  參數5  1024.0
  00453C25  |.  6A FF              push    -1                               ;  參數4
  00453C27  |.  D905 F4125400      fld     dword ptr [5412F4]
  00453C2D  |.  83EC 08            sub     esp, 8
  00453C30  |.  D95C24 04          fstp    dword ptr [esp+4]                ;  參數3  3.0
  00453C34  |.  8BD0               mov     edx, eax
  00453C36  |.  D9EE               fldz
  00453C38  |.  D91C24             fstp    dword ptr [esp]                  ;  參數2  0.0
  00453C3B  |.  56                 push    esi                              ;  參數1
  00453C3C  |.  BE 01000000        mov     esi, 1
  00453C41  |.  8BCE               mov     ecx, esi
  00453C43  |.  E8 08C30A00        call    004FFF50
  
  
  可以看到,參數4是一個固定值,第二、三、五參數都是浮點數。
  
  這裏有一個技巧,在函數開始的地方修改參數的值,看看會發生什麼變化,很快就能知道參數的作用。
  
  參數4是顏色值,Alpha和RGB值都是FF,就是白色,與在遊戲中看到的字符顏色相同。
  參數2是X座標,參數3是Y座標,參數5用於調整。
  需要注意的是,還有兩個參數是通過寄存器ECX和EDX傳遞的,ECX用於表示左對齊(0),右對齊(1)和居中(2),EDX是固定值58D6D0,一個全局的結構或類。
  
  此遊戲有兩種字體,因此用於顯示字符的幾大要素,現在還缺一個,就是不知道如何判斷字符的大小。
  
  在用於顯示文字的004FFF50的上下斷點,多跟幾次,就會發現,每次調用以前,都會調用一個call:
  
代碼:
  00453BD5  |.  BE 44035300        mov     esi, 00530344                    ;  ASCII "zekton16.dds"
  00453BDA  |.  E8 91B50A00        call    004FF170
  
  004479E7  |.  BE 54035300        mov     esi, 00530354                    ;  ASCII "cwfont20.dds"
  004479EC  |.  E8 7F770B00        call    004FF170
  
  在遊戲目錄data\font下面有兩個文件zekton16.dds.dat和cwfont20.dds.dat,由此判斷004FF170應該是用於選擇字體的函數。
  
  總結一下:
  第一步,找到IDirect3DDevice9句柄,用於初始化ID3DXFont。
  第二步,找到顯示文字的函數,用自己的代碼實現之。
  第三步,逐步找到其他需要修改的地方,比如用於得到字符串寬度、高度,指定寬度的字符串換行顯示等函數。
  
  
  【具體實現】
  我用的是注入dll,然後打內存補丁的方式。這樣的好處是便於更新,可以任意修改實現過程,以後遊戲出了新版本,也只要修改幾個地址變量,重新編譯一下就可以了。
  另外一個好處是,不以文件補丁的形式發佈,沒有版權問題。(有人關心這個嗎?)
  
  源碼在這裏:
  http://gsbzhcn.googlecode.com/svn/trunk/src
  
  簡單的介紹一下流程:
  1.CreateProcess啓動遊戲的exe,並使之處於掛起狀態。
  2.VirtualAllocEx在遊戲進程上申請一塊內存,WriteProcessMemory往裏寫入要注入的dll名稱。
  3.GetProcAddress得到LoadLibraryA的地址。
  4.CreateRemoteThread運行LoadLibraryA,注入dll,dll載入時會爲遊戲進程打上補丁。
  6.WaitForSingleObject等候dll載入完成。
  7.VirtualFreeEx清理掉之前申請的內存。
  8.ResumeThread讓打過補丁的遊戲進程運行起來。
  
  內存補丁主要是讓遊戲在關鍵的地方跳轉到我們的dll,執行一段代碼後再跳回去,或者直接用dll裏的函數代替之。
  
  
  【結尾】
  此遊戲的漢化正在http://code.google.com/p/gsbzhcn/ 進行,文本不多,奈何翻譯人手也不多,希望有興趣的同學能夠參與。
  
--------------------------------------------------------------------------------
【版權聲明】: 本文原創於看雪技術論壇, 轉載請註明作者並保持文章的完整, 謝謝!

                                                       2010年05月25日 14:52:11
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章