死んだDune RTSゲームの復活: EmperorLauncher
(wheybags.com)- 2001年のRTS
Emperor: Battle for Duneは、現代のWindowsでは実行・インストール・オンラインプレイのすべてが不安定だったが、EmperorLauncher はそれを再びプレイ可能な状態へ復活させるパッチ - 主な改善点は、高解像度対応、60 FPS制限、直接IPでのオンラインマルチプレイ、協力キャンペーンモード、壊れたインストール手順の回避に集中している
- 実装は、
Emperor.exeの置き換えランチャー、Game.exeを対象にした DLL注入、Microsoft Detours による関数パッチ、Direct3D 7 レンダリングフック、winsock の横取り、簡易WOLサーバーで構成される - オンラインプレイでは、既存のP2P・任意ポート・NAT punching構造を単一のクライアント→サーバー接続へトンネリングし、サーバーホストだけがネットワーク設定を気にすればよいように変更した
- 元のCDファイルのコピー、
.cab展開、公式 v1.09 パッチの適用、WOLAPI.DLLの COM登録回避、Win32 ランチャーUIまでをまとめ、インストールから起動まで処理する
Emperor: Battle for Duneが行き詰まっていた点
Emperor: Battle for Duneは 2001年に Westwood Studios が制作したリアルタイムストラテジーゲームで、Dune 2000の続編- 現代のシステムでは、プレイを妨げる問題が各所に残っている
- 現代の画面に合った 高解像度 で実行できない
- マルチプレイではゲームシミュレーション速度が制限されず、速すぎる
- Westwood Online(WOL) がすでに動作しないため、LAN外でのマルチプレイが難しい
- 協力キャンペーンはオンライン専用機能のため、LANでは使えない
- ディスクに含まれるインストーラーが壊れている
- 現代PCの高いフレームレートでは、さまざまな視覚効果が崩れる
- EmperorLauncher はこれらの問題を解決するためのパッチで、ダウンロードファイルとソースコードが公開されている
Emperor.exe の置き換えと Game.exe の初期化
- ゲームの
Emperor.exeは実際のゲーム実行ファイルではなく、Game.exeを起動する薄いラッパー Game.exeを直接実行しても何も起こらないため、Emperor.exeが行っていた初期化手順を解析し、置き換えランチャーで再現する必要があった- 解析には IDA が使われた
- IDA は実行ファイルを逆アセンブルし、一部のコードをC形式にデコンパイルできる
- 型情報や構造体情報が失われたバイナリから、関数呼び出しや Windows API の使用を追跡しなければならない
Emperor.exeはGame.exe実行前に mutex と匿名ファイルマッピングハンドルを作成し、Emperor.datから読み込んだデータを処理したあと、マッピングへコピーする- 親プロセスは
CreateProcessAで得た子プロセスのメインスレッドIDへ Windows メッセージを送り、ファイルマッピングハンドル値を渡す- カスタムメッセージIDとして
0xBEEFが使われる - ファイルマッピングデータは
"UIDATA,3DDATA,MAPS"の3つの文字列で、ゲームのアセット読み込みコードへ渡される
- カスタムメッセージIDとして
- 復号コードを直接再実装する代わりに、
Game.exeの位置にダンプツールを置いて受け渡しデータをディスクへ保存し、その後ランチャー側で同じシーケンスを実行した
DLL注入と関数パッチの方式
- パッチを適用するには
Game.exeプロセス内でユーザーコードを実行する必要があり、そのために CreateRemoteThread + LoadLibrary 方式が使われた - 注入手順は次の順序で進む
- 対象プロセスのメモリに
VirtualAllocExでバッファを確保する WriteProcessMemoryで DLL パス文字列をコピーするLoadLibraryのアドレスと DLL パスバッファをCreateRemoteThreadに渡し、対象プロセスで DLL をロードする- DLL の
DllMainが実行され、パッチコードが動作する
- 対象プロセスのメモリに
- プロセスを suspended 状態で開始してから DLL を注入し、
main実行前にコードを走らせられるようにした - 既存関数の修正には Microsoft Detours が使われた
- Detours は元の関数先頭命令をジャンプ命令に書き換え、呼び出しを置き換え関数へ送る
- 上書きした元の命令は別メモリへコピーし、その後で元の関数の残り位置へジャンプするラッパーを作ることで、元関数の呼び出しも可能にする
- 関数コードページはセキュリティ上書き込み不可なので、
VirtualProtectで権限を変更し、修正後にFlushInstructionCacheを呼ぶ必要がある
デバッグログの復元
- バイナリ内にはデバッグログらしき呼び出しがあったが、実際の対象関数は
retしかない空関数だった - リリースビルドでは複数の空関数が同じコードにまとめられたようで、そのうちの1つが デバッグロガー だった
- 当初は、最初の引数を文字列ポインタとして解釈し、表示可能な ASCII 文字かどうかを調べるヒューリスティックを使った
- 誤ったポインタアクセスは Windows の SEH例外 で捕捉して無視した
- この方法はある程度動作したが、false positive と false negative が残った
- その後、IDA のパッチ機能と Python スクリプトを使い、ログ呼び出し箇所を別の空関数へ移した
- 一部はヒューリスティックで見つけ、一部は文字列定数を push してから呼ぶパターンで見つけた
- 残る数百の呼び出し箇所は手動で注釈を付けた
- 復元されたログは WOL マルチプレイのデバッグに役立った
SC_MESSAGE_YOUR_DETAILS処理中の"MyId == INVALID_ID"assert ログを見て、GAMEOPTコマンドを全プレイヤーに誤って送っていたことを Wireshark ダンプで確認した
Direct3D 7 グラフィックパッチ
- Emperor は Direct3D 7 ベースのゲームで、現代Windowsの Direct3D 7 サポートは完全ではない
- 高解像度問題は Direct3D 7 ラッパー層の最大テクスチャサイズ 2048 制限と関係しており、UCyborg の LegacyD3DResolutionHack のコードを活用して解決した
- ゲームは 4:3 比率以外の画面を正しく処理できない
- レンダリング自体はできるが、UI が過度に拡大されたように崩れる
- ゲーム内マウス描画のオフセットも、画面中央からの距離に応じてずれる
- 解決策は 4:3 レターボックス化
- ゲームをウィンドウモードで実行すると、任意の解像度を使える
- ウィンドウ枠のスタイルを外し、全画面の黒いウィンドウ上にゲームウィンドウを再親設定する
- マウスキャプチャを追加し、マルチモニターやウィンドウモードでも端スクロールが崩れないようにした
- フレームレート制限は
IDirect3DDevice7::EndSceneをパッチし、60 FPS に合わせたEndSceneはフレーム末尾で1回呼ばれるため、待ち時間を計算してスレッドを sleep させるのに適しているEndSceneポインタは直接 export されていないため、DirectDrawCreateExとIDirect3D7::CreateDeviceの呼び出しを順にフックして vtable から関数ポインタを取得した
オンラインマルチプレイと WOL 代替
- 目標は、ロビーやホスティング基盤なしで、ポートフォワーディングと IP 入力だけで接続する 直接IPマルチプレイ だった
- LAN モードは動作するが、UDP broadcast でサーバーを探すため、インターネットプレイには向かない
- LAN メニューには手動IP入力機能がない
- 当初は LAN チャットをパッチして IP を指定する方法を試したが、協力キャンペーンが WOL 専用だと分かって中止した
- WOL を復活させるには2つが必要だった
- ゲームがどこに接続し、どのゲームを開始するかを把握できるようにする偽の WOLマスターサーバー
- 直接IP接続の上でゲームパケットが動作するようにするプロキシ
- 既存の WOL 構造には master server とともに “mangler” サーバーがあり、mangler は NAT punching の調整役だったようだ
- 元の mangler サーバーは消滅しており、ゲームはその応答を待って停止する
- パッチでは mangler 呼び出しを削除した
- Emperor は P2P ネットワークモデルを使用し、プレイヤーのペアごとに双方向接続を開いて任意ポートを選ぶ
- すべてのクライアントが開放ポートを受け付ける必要がある構造で、現代のインターネット環境には合わない
- 解決策は、winsock 関数を横取りしてすべての接続を 単一のクライアント→サーバー接続 へトンネリングする方法
- クライアントがサーバーや他クライアントへ送ろうとするメッセージを横取りし、ヘッダーで包んで単一接続へ送信する
- サーバー側スレッドがメッセージを受け取り、宛先へ振り分ける
- ゲームは依然として P2P のように動作していると認識するが、実際にはサーバーホストだけがネットワーク設定を扱えばよい
- この構成で協力キャンペーンゲームの開始と参加が可能になった
簡易WOLサーバーの実装
- WOL マスターサーバーは IRC サーバーに近い構造だった
xwis.netにはファン運営と思われるサーバーがあり、執筆時点ではゲーム本来の DNS エントリservserv.westwood.comにもアクセス権を持っているようだった- Emperor が xwis でそのまま正常動作するわけではなかったが、ロビー作成と参加の参考になった
- 公開されている WOL サーバー実装である pvpgn-serverの handle_wol.cpp も参考資料として使われた
- 独自サーバーを作った理由は、外部サーバーが今後も維持される保証がないため
- 目的は競合コミュニティの運営ではなく、マルチプレイゲーム実行に必要な最小機能の提供だった
- WOL は標準 IRC とカスタム動作が混在している
- ゲームロビーは特殊チャンネル
- ロビー情報は IRC topic を使う
- ロビーチャットは
PRIVMSGではなくPAGEを使う - ゲーム設定同期には ASCII ではない内容を含む
GAMEINFOメッセージを使う
- 基本的な WOL サーバー実装は trial and error で完成しており、正常経路を外れると堅牢ではないが動作する
インストーラーと v1.09 パッチ適用
- 元のインストーラーは壊れており、ユーザーが CD 内容をハードディスクへコピーし、代替 setup を上書きする回避策が必要だった
- EmperorLauncher には、元のCDからファイルをコピーし、
.cabファイルを展開するインストール機能が含まれる.cabは zip に似たアーカイブ形式で、Windows には展開用インターフェースがある
- 最後の公式パッチである v1.09 の適用はさらに厄介だった
EM109EN.EXEを 7zip で単純展開して最新バイナリを得る方法は通用しない- Windows resource 内で実行ファイルヘッダーが見える大きなリソースを発見した
- そのリソースの先頭4バイトがファイルサイズで、その後ろが実際のファイルだった
EM109EN.EXEは内蔵 DLL を一時ファイルとして展開してロードし、その後 DLL 内部の関数RTPatch32@12を実行するRTPatchはバイナリ diff パッチツールだった- myRTP ツールを参考に、内蔵 DLL を直接ロードして実行した
- DLL は引数で渡されたインストールパスではなく registry からパスを読んでいたため、期待する registry key を偽装してパッチを適用した
Westwood Online Shared Internet Components の処理
- 元のインストールには Emperor 本体とは別に Westwood Online Shared Internet Components がある
- この構成要素がないと WOL は動作せず、主要ファイルは
WOLAPI.DLL WOLAPI.DLLは COM class library で、Emperor はCoCreateClassにより DLL 内の COM オブジェクトを生成する- 一般的な COM 登録では
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID配下に CLSID と DLL パスをシステム全体へ登録する- この方式には管理者権限が必要
- ゲームプロセスの範囲を超えてシステム全体に影響する
- パッチでは registry redirect と
OaEnablePerUserTLibRegistrationを使い、ユーザー単位の登録方式で処理した- 単一プロセスだけに class library を登録する方法は見つからなかった
DllGetClassObjectを直接使う試みは動作しなかった
ランチャーUIと最終結果
- 最後の段階は、IP入力と基本設定変更のための簡単なランチャーUIだった
- UI は raw Win32 controls で書かれている
- 静的で単純なUIには十分だったが、Win32 UI を直接作る体験はかなり荒削りだった
- EmperorLauncher は最終的に、現代システムでの実行、高解像度、60 FPS制限、直接IPマルチプレイ、協力キャンペーン、インストールとパッチ適用までを含むツールになった
まだコメントはありません。