Another Worldのソースコードの読み取りとリバースエンジニアリングに2週間を費やしました(北米では、このゲームはOut Of This Worldと呼ばれていました)。 私の仕事は、C ++で実行可能なオリジナルバイナリバイナリDOSのグレゴリーモントワールによるリバース開発に基づいています。
リアルタイムのバイトコードを解釈し、フルスクリーンのベクターモーションを生成する仮想マシンで構成されたエレガントなシステムを見つけてショックを受けました。 この作品の結果は、史上最高のゲームの一つでした...
これらはすべて1.44 MBのフロッピーディスクに収まり、600 KBのRAMで動作しました。 1991年も悪くない! いつものように、メモを整理します-これは誰かが数時間の作業を節約するのに役立ちます。
しかし... ...ソースコードは何ですか?
Another Worldのソースコードは公式には公開されておらず、リークもありませんでした。 この革命的なゲームに情熱を傾ける人々は、DOS実行可能ファイルをリバースエンジニアリングしました。
これは、バイナリファイルのサイズが小さいため(20 KB)、部分的に可能でした。 なぜ彼はとても小さかったのですか? ANOTHER.EXEはゲーム自体ではなく、仮想マシンのみであるため:
- バイトコードストレージ。
- システムコールの実行。
バイトコードは、独自のオペレーションコードを使用してすべてのゲームロジックを実行しますが、システムコールを使用して、レンダリング、音楽、サウンドの再生、ゲームリソースの管理などの「重い」タスクを実行します。
目的のOSに仮想マシンのみを実装すると、必要な労力が大幅に軽減されるため、ゲームは12を超えるプラットフォームに移植されました。
- 1991:アミーガ、アタリST
- 1992:Apple IIGS、DOS、SNES、Mega Drive
- 1993:3DO
- 2004:GameBoy Advanced
- 2005:Windows XP、Symbia OS、Windows Mobile
- 2011:iOS
OSの仮想マシンをコンパイルするだけでよい場合は、バイトコードは同じままです。
建築
実行可能ファイルは20 KBしか占有しません。 次のスキームがあります。
ここには4つのモジュールがあります。
- 仮想マシン:システム全体を制御します。
- リソースマネージャー:仮想マシンが要求したときに、フロッピーディスクからリソースを読み込みます。
- サウンド/音楽ミキサー:VMから要求されたときにノイズを混合します。
- レンダラー:VMの要求に応じて頂点を読み取り、レンダリングします。 メモリセグメントから頂点を読み取ります。
興味深い事実:パレットのメモリセグメントには、実際には徐々に色が変化する美しい効果に使用されるいくつかのパレットが含まれています。
起動されると、実行可能ファイルは、仮想マシンストリームのソフトウェアカウンター(
0x00
)に値「0」を設定し、解釈を開始します。 その後、すべてがバイトコードによって制御されます。
可視化の説明
前の図は、3つのフレームバッファーを示しています。 Another Worldはプログラムでダブルバッファリングを実装し、3番目はスマート最適化として使用されるため、2つが必要です。
3番目のフレームバッファーは、背景とシーンを合成するために1回だけ使用され、単純な
memcpy
してフレームごとに再適用します。
このビデオでは、別の世界の伝説的な第1レベルの画面の視覚化が遅くなり、レンダリングプロセスに気付くことができます。 すべてはポリゴンとピクシゴンを使用して描画されます。 再描画の回数は非常に重要ですが、シーンは1回しか生成されないため、それほど悪くはありません。
興味深い事実:この有名な背景は981個のポリゴンで構成されています。
全体像を視覚化するために、3つのフレームバッファーと画面に表示されるものを遅くしてレンダリングしました。
ここでは非常にはっきりと見ることができます:
- フロント/リアバッファで順番に実行される視覚化によるダブルバッファリング。
- バックグラウンドバッファは一度生成され、左上のバッファに保存されます。 次に、各フレームの先頭にコピーされます。
- バックグラウンドが変更された場合(たとえば、車が停止した場合)、バックグラウンドバッファーが更新され、スペースと時間をさらに節約できます。
レンダリングをより詳細に分析したい場合は、ここに完全なビデオがあります。
別の世界の仮想マシン
Eric Chahiのページでは、VMの構造について詳しく説明しています。
githubコードでは、各オペレーションコードの実装方法を確認できます。 それらはすべて、視覚化操作を除いて非常に簡単に理解できます。 秘Theは、頂点が読み込まれるポリゴンセグメントのソースコードにオペレーションコードの識別子が埋め込まれていることです。
以下は、VMバイトコードエディターのスクリーンショットです(エリックシャイが作成し、「スクリプトエディター」という名前を付けました)。
ここで、ラベルがどのように失われるかを
setvec 21 nag1
でき
setvec 21 nag1
は、スレッド21の命令カウンターを「nag1」ラベルのオフセットに設定します。 バイトコードでは、ハードコード化されたオフセットのみが表示されます。
トランザクションコードの使用例
以下の図は、仮想マシンによるオペレーションコードの呼び出しを示しています。これは、実際には4つのメモリセグメントをロードするリソースマネージャーへのシステムコールです。 これは通常、ゲームの開始時に発生します(ゲーム全体は10の部分に分かれています)。
次の図では、操作コードは頂点の描画と取得を要求するレンダリングシステムコールでもあります。 視覚化操作コードは、頂点を読み取る必要があるアドレスの指示が含まれているため、もう少し複雑です。 宛先フレームバッファの選択は、完全に独立したオペレーションコードです。
注:レンダラーが頂点を(運動学的ポリゴンのセグメントまたはアニメーションセグメントから)読み込む場所の選択は、opcodeIdを使用してエンコードされます。
リソース管理
リソースは一意の整数識別子で識別されます。 起動すると、リソースマネージャはMEMLIST.BINを開き、次のようにエントリを取得します。
typedef struct memEntry_s { int bankId; int offset; int size; int unpackedSize; } memEntry_t;
VMがresourceIdを要求する場合、リソースマネージャーは次のことを行います。
- 銀行ファイルを開いて(bankIdによって)見つけます。
-
offset
をスキップし、RAMのsize
バイトを読み取ります。 -
size
!=unpackedSize
場合、リソースを解凍する必要があります。
圧縮に関するいくつかの統計:
: 146 : 120 : 28 : 82% . () : 1820901 . () : 1236519 . : : 32%. Total RT_SOUND : 699868 (38% ) 585052 (47% ) : (16%) Total RT_MUSIC : 33344 (2% ) 3540 (0% ) : (89%) Total RT_POLY_ANIM : 384000 (21% ) 106676 (9% ) : (72%) Total RT_PALETTE : 18432 (1% ) 11032 (1% ) : (40%) Total RT_BYTECODE : 203546 (11% ) 135948 (11% ) : (33%) Total RT_POLY_CINEMATIC : 365960 (20% ) 291008 (24% ) : (20%) : ! : 148 Total RT_SOUND: 103 Total RT_MUSIC: 3 Total RT_POLY_ANIM: 12 Total RT_PALETTE: 9 Total RT_BYTECODE: 9 Total RT_POLY_CINEMATIC: 9
圧縮アルゴリズムの逆の開発に時間を費やしませんでした。 音が非常にうまく圧縮されなかったという事実は、アルゴリズムがエントロピーに敏感であると考えるようになりました...したがって、おそらくこれはハフマンアルゴリズムのバリエーションですか?
146個のリソースのうち、120個が圧縮されています。
- ベクトル可視化とその上での圧縮により、大きなメリットが得られました(スペースを最大62%節約できます!)。
- 音声圧縮は非常に非効率的です。節約量は少なく、リソースはディスク容量の47%を占めます。
興味深い事実:圧縮後3分間続く最初のスプラッシュスクリーン(リソース0x1C )は57 510バイトしかかかりません。
メモリ管理
90年代のすべてのゲームと同様、ゲームプレイ中にメモリは割り当てられませんでした。 起動時に、ゲームエンジンは600 KBのメモリを受け取りました(640 KBのDOSベースメモリを覚えている人はいますか?)。 600 KBがスタックディスペンサーとして使用されました。
空きメモリ:メモリマネージャは、1サイクル前にメモリを解放するか、すべてのメモリを解放できました。 実際には、ゲームの10のパートのそれぞれの終わりにすべてのメモリが解放されました。
興味深い事実:最初はすべての600 KBにバイトコードと頂点が含まれていました。 しかし、ポリゴン/ピクセルから2年間背景を生成した後、ゲームはまだ終わっていませんでした。 開発をスピードアップするために、Eric Shayiは彼の素晴らしいアーキテクチャにハックを導入することを決定しました(パフォーマンスの低下を犠牲にして):リソースマネージャーは、フロッピーディスクからバックグラウンドイメージのビットマップをバックグラウンドバッファーに読み込むことができます(
void copyToBackgroundBuffer(const uint8 *src);
)
void copyToBackgroundBuffer(const uint8 *src);
)。 したがって、ベースメモリの最後に32KB(320x200 / 2)が予約されました。
興味深い事実:このハックは、2005年にWindows XPでAnother Worldがリリースされたときに使用されました。 すべての背景は手で描かれ、レンダラーとそのピクシゴンを使用せずにハードディスクから直接ロードされました。
純粋主義者のコーナー
あなたが純粋主義者であり、オリジナルバージョンでのみプレイしたい場合、Another WorldはDosBOXで素晴らしい動作をします。
または、Windows XP用のバージョンを再生できます。 Collector's editionを購入することをお勧めします。これには、Eric Shayaのテクニカルノートを含む多くの追加情報が含まれています。
そしてもう一つ
理解しやすくするために、コードに一生懸命取り組みました。 これがどれほど明確になったかの例を次に示します。
宛先:
void Logic::runScripts() { for (int i = 0; i < 0x40; ++i) { if (_scriptPaused[0][i] == 0) { uint16 n = _scriptSlotsPos[0][i]; if (n != 0xFFFF) { _scriptPtr.pc = _res->_segCode + n; _stackPtr = 0; _scriptHalted = false; debug(DBG_LOGIC, "Logic::runScripts() i=0x%02X n=0x%02X *p=0x%02X", i, n, *_scriptPtr.pc); executeScript(); _scriptSlotsPos[0][i] = _scriptPtr.pc - _res->_segCode; debug(DBG_LOGIC, "Logic::runScripts() i=0x%02X pos=0x%X", i, _scriptSlotsPos[0][i]); if (_stub->_pi.quit) { break; } } } } }
後:
void VirtualMachine::hostFrame() { // ( ). // 0xFFFF (VM_INACTIVE_THREAD). // , . for (int threadId = 0; threadId < VM_NUM_THREADS; threadId++) { if (!vmIsChannelActive[CURR_STATE][threadId]) continue; uint16 pcOffset = threadsData[PC_OFFSET][threadId]; if (pcOffset != VM_INACTIVE_THREAD) { // . // pc executeThread // . _scriptPtr.pc = res->segBytecode + pcOffset; _stackPtr = 0; gotoNextThread = false; debug(DBG_VM, "VirtualMachine::hostFrame() i=0x%02X n=0x%02X *p=0x%02X", threadId, n, *_scriptPtr.pc); executeThread(); // .pc , . threadsData[PC_OFFSET][threadId] = _scriptPtr.pc - res->segBytecode; debug(DBG_VM, "VirtualMachine::hostFrame() i=0x%02X pos=0x%X", threadId, threadsData[0][threadId]); if (sys->input.quit) { break; } } } }
- マクロを使用して、絡み合ったハードコードされた値を取り除きました。
- 変数の名前を変更しました。
- 多くのコメントを追加しました。
これが「人間が読める」ソースコードです。 ハッキングに成功しました。