
2026/11/05 3:51
Show HN: 10年かけて逆向きにした後ついに、DOS版『ウィングコマンド』の協力プレイが動作するようになりました
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
オリジナルのサマリーはハイレベルな概要には適しているが、キーポイントリストに含まれているような技術的な深みは欠けている。以下は、物語の流れを保持しつつ、リストから具体的な技術的マイルストーンと年代順の文脈を取り込んだ改良版である。
改善されたサマリー:
最も重要な教訓は、2人の開発者(Daniel Rh と Patrick)が、1990 年代のシングルプレイヤー DOS ゲーム『Wing Commander』を厳密な制限(640K RAM の旧ハードウェア上)の中でスタートし、11 年間の旅程(2015–2026)を通じて完全に機能する協力型マルチプレイヤー体験へと成功裏に変容させた点である。コアファイルを変更せずにマルチプレイヤー機能を注入するためには、内部メモリ構造を分析するために DOSBox を利用し、プレイヤーの位置、速度、勝利点を制御する特定のデータアドレスを 64K データセグメント(dseg)内で特定した。コアファイルを変更せずにマルチプレイヤー機能を注入するため、「死んだ文字列」を使用して Borland オーバーレイを通じてゲーム関数を呼び出す技術や、複雑な_trampoline システムを開拓した。開発は TCP/protobuf ネットワーキングプロトコルの実装から始まり、エンティティスロットの競合を解決し、VDUフックによるインゲームテキストチャットの導入へと進化した。爆発やオートパイロットアニメーションなどの動的要素に対するサポートを洗練させた後、プロジェクトは 2026 年のアップデートで Emscripten を使用して再構築された形で完成した。この最終リリースには、ブラウザベースの WebRTC ピアツーピア接続、ボイチャット、カスタムロビーシステムが搭載されており、高価なモダンポートではなく高度なリバースエンジニアリングを通じてレガシーソフトウェアを蘇らせることができることを証明している。
本文
「Wing Commander」共同プレイ実現までの 11 ヶ年と、今さらながらの冒険
導入:想い出と目標
- 子供時代、弟のパトリックとは**一つの機体に二人乗りする「二頭のヒドラ」**のようにではなく、仲の良い翼友として協業プレイしたいと願ってきました。
- ブラウザで遊ぶ体験を通じて、この長年の夢を実現しました。
探求の始まり:2015 年 -2016 年
- 2015 年 10 月:DOSbox でクラシックな『Wing Commander』を遊んでいた際、メモリがビル・ゲイツが冗談半分で語った640Kに収まることに気づきました。
引用元の発言:「1981 年にその決定を下す際、私は今後 10 年は十分使える自由度を与えられていると感じていました。つまり、64K から 640K という変化は長らく続くものと見込まれました。しかし、そうはなりませんでした」(出典)
- アイデア:メモリをダンプしてプレイヤーと翼友の座標(X, Y, Z)を特定し、ネットワークパケットで翼友を制御する仕組みを作ればよいと考えました。
メモリ構成の詳細
- 主要なメモリ領域:
:データセグメント(64K)DS:13D3
:VGA ビデオメモリA000
:EMS ページフレームE000
:終了アドレスFFFF:F
- 興味深いアドレスと格納内容:
:デッドストリング(使われなくなった文字列領域)0x0187
:ランダムシード0x7728
:プレイヤー位置情報0xA9C2
:ミッションツリー0xD248
:通信テキスト0x8DF8
:速度ベクトル0xCAE2
:スタック領域0xFFxx
:右・上・前方のベクトル方向量0xAEB6〜B1B6・B4B6
問題解決:メモリ差分解析とアプローチ
- 本質:わずか 100 万バイト未満の中から、ゲームステートを格納している場所を特定する必要がありました。
- 手法:
- ゲーム内の他のエンティティを全て消去して単一化。
- プレイヤーが止まっている状態と移動後の状態のメモリの差分を撮影。
- 「背景」として変化しないメモリ領域を特定。
- DOSbox の
コマンドでダンプを取得し、エンティティ消滅時に位置情報を記録。MEMDUMPBIN
データセグメント内の差分解析結果
| 比較項目 | 変動量 | 備考 |
|---|---|---|
| 停止状態 | 253 バイト | 変化なしの領域も含まれる |
| 移動時 | 650 バイト | 追加で変化する領域あり |
| 両者共通 | 413 バイト | 真正なゲームステート候補 |
| 追跡カメラビュー参照後 | 132 バイト | 実効的な変化に絞った結果 |
- 解析の深化:
- データセグメント内で 35 の変更クラスタを特定し、視覚的検査で 12 カ所に絞り込みました。
- 最終的に
というアドレスに収束しました。13D3:A9C2
- 検証:
- DOSbox デバッガーの
コマンドでこのアドレスに整数を書き込むと、船が前方へジャンプ移動することが確認できました。SM - 同様に
アドレスでの速度変化も特定(停止時のゼロ値を持つ 32 ビット整数が 3 つ存在)。CAE2
- DOSbox デバッガーの
ロータリングの謎解き:ベクトルとオーバーレイ
- 課題:回転ベクトルの格納位置の詳細な分析が必要でした。
- ベクトルは
とみなす 4 バイトのスカラー構成で、有効値は256 = 1.0
の範囲。0〜255 - 微細な回転では単一バイトしか変化しないため、特定が困難でした。
- ベクトルは
- 発見:開発者によるユーモアのような構造が存在していました。
- エンティティプロパティ用に 64 スロット(
〜0x00
)を予約。0x3F - デベロッパーは
を一時用途、カメラを0x3F
に配置しました。0x3D - これにより、カメラ変数から 2 スロット離れた一時的な変数が誕生し、
やup
も隣り合う構造になりました。forward
- エンティティプロパティ用に 64 スロット(
回転ベクトルの位置特定
| タイプ | アドレス (船コピー) | アドレス (カメラコピー) | 備考 |
|---|---|---|---|
| up[0] | | | 毎フレーム更新 |
| up[空スロット] | - | | - |
| up[一時変数] | - | | スロット |
| forward[0] | | - | 星雲背景のシフト可視化 |
- 小端表現(Little-Endian):非ゼロ単位ベクトルは
(256=1.0)00 01 00 00 - 遷移確認:
からlevel flight
への遷移でyaw
が 9 バイト変化。forward[0]
がup[0]
から00 01
に変化した。7E 00
コード追跡とフック地点の発見
- ID(IDA)でのバイトシーケンス検索と注釈付けを開始。
- BPM(Break Point Monitor)で監視:
:DOSbox フック地点CS:IP == 0560:20E3- 呼び出し連鎖追跡:
→vector_add
→physics per ship
→per framemain loop
制御機構の構築:トリムポリン設計
- 限界の認識:「グローバルメモリへの書き込みだけで操作できる」という幻想は砕け、元のゲーム関数を流用して正確な体験を得る必要があります。
- 手法:プログラムカウンターを正しい場所へ誘導。引数を挿入し元に戻ってくるメカニズムを実現。
スタブによる関数呼び出し(例:fireAllGuns)
- オーバーレイ 143 (
) のセグメントstub143
に JMP far スタブが配置されています。12D7
→12D7:0084
(jmp far 2F31:0A99
)do_damage
→12D7:012E
(jmp far 2F31:2978
)fireGunFromShip
→12D7:0156
(jmp far 2F31:2E76
)fireAllGuns
実行フロー
- DOSbox が
をスタックにプッシュし、指示ポインタを上記文字列リロケート。CS:IP - 0x00:終止文字列。
:レジスタ状態保持。pushf; pusha
:引数設定(シップ 0)。xor si,si
:スタブ経由で関数呼び出し。call far 12D7:0156 via stub
:cdecl で戻り値処理。pop si; popa; popf
:DOSbox がプッシュしたアドレスへ復旧。retf
- この操作により、レーザーキャノンの発射が確認できました。
- 2017 年 3 月:メインループの予測可能なフック地点(
)発見。不確定部分を回避。0560:20E3 - 同年 7 月:
関数を特定し、セカンドプレイヤーへの呼び出しを阻止。do_damage
ネットワーク統合とネットワークプロトコル
- 2017 年 8 月:簡素な TCP クライアント/サーバーアーキテクチャと protobuf ゲームメッセージを実装。
- リーダーから全ての船位置、発射イベント、消滅、ダメージを配信。
- クライアントは自身の位置や大事件(射出・着陸・死亡)を返信。
初期問題:スタック処理とオーバーレイローダー
- 問題:Borland のオーバーレイローダーがコードページをスワップする際、自作スタブにはフレームが存在せず、ゴミ値を読み取りゲームがランダムにクラッシュしました。
- 解決策:Borland 関数のようなスタブを作成し、
で対応。push bp; mov bp, sp; nop (0x90); pop bp; retf- DOSbox が
に到達した際に、ジョブキューから次のコールをインジェクトするように設定。nop
- DOSbox が
スロットと ID マッピングの統一
- 2017 年 10 月:一時的オブジェクトがプレイヤーごとに異なるスロットに配置されていることを発見。
- スロット
:プレイヤー0 - スロット
:翼友1 - スロット
:カメラ3D - スロット
:一時スロット3F
- スロット
- 対応策:一時的オブジェクトの共有領域(スロット 12〜15)を制限し、マルチプレイヤー位置の一貫性を保つように変更。
- 2017 年 12 月:クライアントをサーバー船 ID とローカル ID のマッピング格納に変更。
関数の復帰強制不要化により、パケット変換が可能になり、小惑石のマルチプレイも動作。spoon
ゲーム内容の拡張と成功
- 2018 年:爆発、ミサイル、オートパイロットのアニメーションに注力。
- 2019 年 6 月:ミッション勝利計算が不安定であることを発見。
- バイナリ探索と半減法を繰り返して実際の集計位置を特定。
- 最終的に
の 80 バイトにミッションツリー、13D3:D248
に総合勝利ポイントカウンターを見つけました。13D3:C280 - これにより、数年後のドロップダウンでのミッション選択が可能になりました。
通信テキストの統合
- 2020 年初頭:通信テキスト再生関数を特定し、ネットワーク経由でのチャット送信を実現。
- タイピングされたチャットを翼友の VDU で表示させるシステムフック完成。
- **「両方のパイロットがタイピングし、互いにメッセージを送る」**体験が可能に。
試行錯誤の記録とタイムライン
| 期間 | 主な成果・発見 |
|---|---|
| 2016 | メモリダンプ取得・位置情報の特定 |
| 2016.9 | スナップショット比較ツールの開発 |
| 2016.11 | 右/up/forward ベクトル、スロット の特定 () |
| 2016.12-2017.3 | コール注入・メインループフックの確立 |
| 2017.7 | 関数の特定 |
| 2017.8 | TCP over protobuf の実装 |
| 2017.10-12 | トランポリン設計、push bp、スロット制限 ()、ID マッピング |
| 2019.6-7 | バイナリ探索による D248(勝利ポイント)の発見 |
| 2019.10 | 総合勝利ポイント・ミッション番号の特定 |
| 2020.1 | 通信テキスト機能の実装 |
| 2020.3 | コミュニケーションパネルでのチャット完成 |
| 2020.10 | 遊べる状態への到達 |
プロジェクトの復活:lobbylink と現代技術による再構築
- 課題:友人との共有が困難(古いコピー探し、DOSbox 変種コンパイル、IP 共有など)。
- 解決策:
- WebRTC の普及と堅牢な emscripten サポートを活用。
- lobbylink システムを開発し、プロジェクトを復活・共有可能にしました。
lobbylink の概要
- 実装:https://github.com/danielrh/lobbylink(golang ベース)。
- 依存:Websocket ライブラリのみ。
- 機能:
- ピアツーピア WebRTC 接続を調整。
- 小さなサーバー上で動作し、共有ルームコードを通じてプレイヤーが接続。
- 静的なオリジンリストを持つため、任意のブラウザやクライアントソフトでも接続可能。
- サーバー側で coturn を実行し、TURN クレデンシャルを発行するため、制限的なファイアウォール背後からの接続もサポート。
接続フロー
- join:ルームコード入力。
- joined:プレイヤー 0 が接続(ICE サーバー情報取得)。
- player-joined:他プレイヤーの加入通知。
- データチャネルオープン:ゲームパケット・音声通信開始。
ブラウザでの遊び方
- GOG の Wing Commander インストーラーを zip 展開し、emscripten データファイルとして使用。
- https://fireflyk64.github.io/dosbox/ にアクセス。
- ゲームファイルをドラッグ&ドロップ。
- ルームコードを入力して接続。
最終的な拡張:音声チャット
- LLM を活用し、オプションで第 2 の WebRTC ピア接続を使ってリアルタイム音声通信を追加。
- これにより、「一緒に飛行し、共に笑い合う」体験が実現しました。