この記事は、Raspberry Pi Advent Calendar 2022の15日目の記事です。
USB Raw GadgetというLinux kernelの機能を使ってUSBモデムをエミュレーションするプログラムを実装し、Raspberry Pi上で実行することで、PlayStation 2専用ゲーム「アーマード・コア2 アナザーエイジ」のモデム対戦をTCP/IPネットワーク上で利用できるようにした話をします。
記事の前半では、開発に至るまでの経緯と、PS2専用モデムのプロトコル通信の解析について説明をします。
USB Raw Gadgetのことだけ知りたい、という場合には目次から「USB Raw GadgetによるUSBモデムエミュレータの実装」の項へ飛んでください。
開発に至るまで
フロム・ソフトウェアが開発した「アーマード・コア」シリーズの作品のうち、「アーマード・コア2 アナザーエイジ」「アーマード・コア3」「アーマード・コア3 サイレントライン」 (以下、「AC2AA」「AC3」「AC3SL」とそれぞれ略す) にはモデム対戦機能があります。
これは、PlayStation 2に専用のUSBモデムを接続することでアナログ電話回線経由で通信し、遠隔地にいるプレイヤーと対戦プレイが行える機能です。
現代においてオンライン対戦ゲームはインターネット経由で通信を行うものが一般的かと思います。
日本におけるインターネットの普及は、2000年代前半にADSL回線の普及が進み常時接続が可能なブロードバンド回線が一般家庭でも利用可能なることで急激に進みました1。
ブロードバンド回線の普及以前、家庭用ゲーム機ではオンラインで通信をする場合、モデムを用いてアナログ電話回線経由で通信を行うものが一般的でした。
AC2AA、AC3、AC3SLの3作は2001年~2003年にかけて発売されており、ちょうどインターネットやブロードバンド回線の普及が進んだ時期と被っていますが、モデムを用いた通信のみ対応となっています。
ブロードバンド回線と比較すると、アナログ電話回線は使用時間と距離に応じた従量課金であること、通信中は1つの機器で回線を専有するなど不便な点が多いです。
現代においては、不便なアナログ電話回線ではなく、ブロードバンド回線を利用してオンライン対戦ができた方が嬉しいです。
2022年現在でもAC2AA等の対戦をPS2の実機で行っているコミュニティでは、モデムが出力した音声信号を、VoIPアダプタを用いてデジタル化しIPネットワーク上で転送する手法が使われています。
しかし、モデムが前提としているアナログ電話回線とは異なり、VoIPではパケット伝送遅延の揺らぎ (ジッター) や、VoIP機器間のクロックが非同期であるために生じるバッファアンダーランやバッファオーバーランにより音声の連続性が保てない2ため、長時間の通信ではエラーが生じやすくなっています。
また、モデムの変調・復調処理や、VoIPによるパケット化、VoIPの安定化のためのバッファリングなど複数の要因が重なることで通信遅延も大きくなっており、いわゆるシングルプレイの場合と比べて9フレーム程度の遅延が発生していました.
(この辺りの試行錯誤については、「KLab Tech Book Vol. 9」の「6. モデム通信しかサポートしていない昔のネット対戦ゲームをブロードバンド・光回線で行うには」という筆 智章氏の記事で説明されています。無料で読めるので興味のあるかたはぜひご覧ください。)
そこで、より快適なネット対戦環境を実現するために、PS2からUSB経由で送られた通信データをUSBモデムの代わりにRaspberry Piで受け取り、デジタルデータのままTCP/IPで送信する方法を考案し、実装しました。
USBモデムの通信プロトコルを解析する
PS2専用モデムは何機種かありますが、AC2AAが正式対応をしているモデムは以下の3機種になります。
- IO-DATA P2GATE
- サン電子 SUNTAC Online Station
- オムロン電子 ME56PS2
今回はオムロン電子のME56PS2を解析することにしました。
この機種を選んだ理由は、5chのアナログモデムスレのある書き込みから、FTDIのチップを使用していると推測したためです。
FTDIは電子工作でマイコンを扱う人にとってはお馴染みの、USBシリアル変換IC FT232シリーズを開発した会社です。
FTDIのUSBシリアル変換ICはLinux kernelでもftdi_sioドライバにより利用できるため、Linux kernelのソースコードから通信プロトコルの情報が得られるのではないかと考えました。
usb-proxyを使ったUSBパケットキャプチャ
USBモデムの通信プロトコルを解析するため、PS2とUSBモデムの間にRaspberry Pi 4 Model Bを接続し、Aristo Chen氏が開発したusb-proxyを実行してUSB通信をラズパイ経由で中継させ、Linux kernelのusbmonモジュールとtcpdumpを用いてパケットダンプを取得しました。
この方法については、別の記事に記載しているので、そちらを参照していただければと思います。
USBデバイスは、自分がどのようなデバイスであるかを説明するデスクリプタと呼ばれる構造体を持っています。
まずはこのデスクリプタを取得し、内容を確認してみました。
デスクリプタの取得と内容の確認
パケットダンプからデスクリプタを含む2つのパケットを取得しました。
Addr Hex ASCII
-----+-------------------------------------------------+-----------------
0000 12 01 10 01 00 00 00 08 90 05 1a 00 01 01 01 02 ................
0010 03 01 ..
DEVICE DESCRIPTOR
bLength: 18
bDescriptorType: 0x01 (DEVICE)
bcdUSB: 0x0110
bDeviceClass: Device (0x00)
bDeviceSubClass: 0
bDeviceProtocol: 0 (Use class code info from Interface Descriptors)
bMaxPacketSize0: 8
idVendor: Omron Corp. (0x0590)
idProduct: Unknown (0x001a)
bcdDevice: 0x0101
iManufacturer: 1
iProduct: 2
iSerialNumber: 3
bNumConfigurations: 1
一つ目はデバイスデスクリプタを含むパケットです。
Vendor ID = 0x0590, Product ID = 0x001aであることや、USB 1.1対応デバイスであることが分かります。
Addr Hex ASCII
-----+-------------------------------------------------+-----------------
0000 09 02 20 00 01 01 02 20 1e 09 04 00 00 02 ff ff .. .... ........
0010 ff 02 07 05 82 02 40 00 00 07 05 02 02 40 00 00 ......@......@..
CONFIGURATION DESCRIPTOR
bLength: 9
bDescriptorType: 0x02 (CONFIGURATION)
wTotalLength: 32
bNumInterfaces: 1
bConfigurationValue: 1
iConfiguration: 2
Configuration bmAttributes: 0x20 NOT SELF-POWERED REMOTE-WAKEUP
bMaxPower: 30 (60mA)
INTERFACE DESCRIPTOR (0.0): class Vendor Specific
bLength: 9
bDescriptorType: 0x04 (INTERFACE)
bInterfaceNumber: 0
bAlternateSetting: 0
bNumEndpoints: 2
bInterfaceClass: Vendor Specific (0xff)
bInterfaceSubClass: 0xff
bInterfaceProtocol: 0xff
iInterface: 2
ENDPOINT DESCRIPTOR
bLength: 7
bDescriptorType: 0x05 (ENDPOINT)
bEndpointAddress: 0x82 IN Endpoint:2
bmAttributes: 0x02
wMaxPacketSize: 64
bInterval: 0
ENDPOINT DESCRIPTOR
bLength: 7
bDescriptorType: 0x05 (ENDPOINT)
bEndpointAddress: 0x02 OUT Endpoint:2
bmAttributes: 0x02
wMaxPacketSize: 64
bInterval: 0
二つ目は、コンフィグレーションデスクリプタ、インターフェイスデスクリプタとエンドポイントデスクリプタ を含むパケットです。
USBデバイスは、一つのデバイスに対して複数のコンフィギュレーションを定義でき、さらにコンフィギュレーション内で複数のインターフェイスを定義できます。
今回解析したUSBモデムでは、コンフィギュレーションとインターフェイスはどちらも1つずつ定義されています。
bInterfaceClassはベンダ定義となっており、USBの規格で定められているモデム用デバイスクラスである USB CDC (Communication Device Class) では無いようです。
エンドポイント構成としては、エンドポイント番号2に IN (device to host) と OUT (host to device) の二つのバルク転送用のエンドポイントを使用するようです。
さらに、usb-proxyによるパケットキャプチャを続けます。
次はエンドポイント2を流れるパケットに注目してみます。
通信パケットの分析
アイドル時、エンドポイント2のIN方向で以下の2バイトのパケットが40ms間隔で送信されています。
Addr Hex ASCII
-----+-------------------------------------------------+------------
0000 31 60 1`
ゲームを操作し接続処理を開始すると、以下のパケットが観測できました。
Addr Hex ASCII
-----+-------------------------------------------------+------------
OUT:
0000 15 41 54 26 46 0d .AT&F.
IN:
0000 31 00 1.
IN:
0000 31 60 1`
IN:
0000 31 60 0d 0a 4f 4b 0d 0a 1`..OK..
IN:
0000 31 60 1`
AT&F
は、モデムの制御に一般的に使われるATコマンド3の一つで、設定の初期化コマンドです。
OK
は、ATコマンドに対するモデムの返答です。
OUT 方向では、1バイトのヘッダの後にペイロードを結合し、IN 方向では 2バイトのヘッダの後にペイロードを結合しているようです。
まずはIN方向のヘッダについて考えてみます。
IN方向のヘッダは 0x31 0x60
と 0x31 0x00
の二種類が見られました。コマンド送信直後の1回だけ 0x31 0x00
が帰ってくるということは、バッファの空き状態を示すフラグの可能性がありそうです。
また、IN方向のヘッダはペイロード長によって変化することは無いようです。
処理が進んで対向のモデムとの接続が確立すると、IN方向のヘッダは 0xb1 0x60
に変化しました。
おそらく、先頭1バイトの最上位ビットは接続状態を示すフラグのようです。
次に、OUT 方向のヘッダを調べるため、OUT方向のパケットをいくつか集めてみます。
Addr Hex ASCII
-----+-------------------------------------------------+------------
OUT:
0000 15 41 54 26 46 0d .AT&F.
OUT:
0000 15 41 54 4d 30 0d .ATM0.
OUT:
0000 19 41 54 25 43 30 0d .AT%C0.
OUT:
0000 15 41 54 45 30 0d .ATE0.
OUT:
0000 0d 41 54 53 .ATS
OUT:
0000 15 33 38 3d 30 0d .38=0.
OUT:
0000 19 41 54 44 54 30 31 .ATDT01
OUT:
0000 25 32 33 34 35 36 37 38 39 0d %23456789.
ペイロードの長さが同じ場合、ヘッダは一致しています。
ペイロードが3バイトの際には 0x0d
、5バイトの際には 0x15
、6バイトの際には0x19
、9バイトの際には0x25
となっています。
どうやら、OUT方向のヘッダの値は ペイロード長 * 4 + 1
と一致するようです。即ち、上位6bitがペイロード長、下位2bitは01
固定となっているようです。
以上から、ヘッダの各ビットは次のような構成であると推測しました。
IN:
X100 0001 0XX0 0000
--------------------+-------------
X 接続ステータス (1 = 接続済み, 0 = 未接続)
011 0001 0 不明
XX 通信ステータス (11 = バッファが空いている?, 00 = バッファが埋まっている?)
0 0000 不明
OUT:
XXXX XX01
--------------------+-------------
XXXX XX ペイロードサイズ
01 不明
プロトコルの答え合わせ
Linux kernelのftdi_sio.hに、FTDIのシリアル変換ICで使われているデータフォーマットに関する説明があります。
IN方向のヘッダについて、0バイト目の最上位ビットは Receive Line Signal Detect (対向のモデムからキャリア信号が検出されたことを示すフラグ) であり、接続状態を示すもので合っているようです。
また、1バイト目の5ビット、6ビット目は送信バッファの空き状態を示すフラグで合っているようです。
OUT方向のヘッダについて、上位6bitがペイロード長、下位2bitは01
固定で合っているようです。
推測したヘッダの構成が ftdi_sio.h に記載されているものと一致するため、解析対象のUSBモデムはFTDIのシリアル変換ICで使われているプロトコルと同じものを使っていると考えて良さそうです。
USB Raw GadgetによるUSBモデムエミュレータの実装
これまでの解析で、USBモデムの通信プロトコルは判明しました。
次は、USB Raw Gadgetを使ってUSBモデムをエミュレーションするプログラムを実装します。
USB Raw Gadgetとは
USB Raw Gadgetは、Linuxを用いてUSBデバイスを実装する場合に、低レベルな通信処理をユーザ空間のアプリケーションで実装できるようにするためのAPIです。
USBで接続する二つの機器は、何らかの機能を提供する「デバイス」と、そのデバイスを利用する「ホスト」に分けることができます。
例えば、パソコン本体やゲーム機がホストであり、マウスやキーボード、ゲームコントローラーなどの周辺機器がデバイスとして振舞います。
Raspberry Piにマウスやキーボードを繋いで使用する際も、Raspberry Piはホストとして振舞います。
パソコンに搭載されているUSBコントローラは、多くの場合はUSBホストとしてのみ動作するため、デバイスになることはありません。
しかし、Raspberry Piのような組込み向けSoCに搭載されるUSBコントローラは、デバイスとしても動作できるようになっています。
LinuxではUSBデバイスの実装をするためのAPIとしてUSB Gadget API for Linuxが用意されています。
また、そのAPIを使ったUSBデバイスクラスの実装としてGadget driversがあり、例えばキーボードやマウスとして振舞うためのHID gadget driverや、ストレージデバイスとして振舞うためのMass Storage GadgetなどのドライバがLinuxに組み込まれています。
しかし、今回のUSBモデムのようにベンダー独自のクラスで動作するものもあるため、既存のGadget driversですべてのデバイスがエミュレーションできるわけではありません。
カーネルモジュールを自分で書くことで独自のgadget driverを実装できますが、カーネルモジュールの開発はユーザー空間のアプリケーション開発に比べると難易度が高いです。
そこで、通信処理をユーザ空間のアプリケーションから制御できるUSB Raw Gadgetを使うことで、カーネルモジュールを書くことなく独自のUSBデバイスを実装しました。
USB Raw Gadgetの使い方
raw_gadgetモジュールが利用可能な場合、/dev/raw-gadget
デバイスが見えます。
USB Raw Gadgetを使うプログラムは、このデバイスに対してioctlを発行することでUSB Device Controller (UDC)とやり取りをします。
USB Raw Gadgetの初期化
初期化に使うリクエストは USB_RAW_IOCTL_INIT
と USB_RAW_IOCTL_RUN
の二つです。
USB_RAW_IOCTL_INIT
は引数に UDC のドライバ名とデバイス名を引数に取ります。
int fd = open("/dev/raw-gadget", O_RDWR);
// INIT
struct usb_raw_init arg;
strcpy((char *) arg.driver_name, "fe980000.usb");
strcpy((char *) arg.device_name, "fe980000.usb");
arg.speed = USB_SPEED_HIGH;
ioctl(fd, USB_RAW_IOCTL_INIT, &arg);
// RUN
ioctl(fd, USB_RAW_IOCTL_RUN, 0);
(エラー処理は省略しています)
イベントの処理
初期化後は、イベントを受け取って処理をします。
イベントの受け取りには USB_RAW_IOCTL_EVENT_FETCH
を使います。
イベントには主に二つの種類があります。
一つ目は USB_RAW_EVENT_CONNECT
で、ドライバがUDCにバインドされた際に発行されます。
ドキュメントによれば、この際に USB_RAW_IOCTL_EPS_INFO
を用いて利用可能なエンドポイントの情報を取得し、エンドポイント番号を決める必要があるそうです。
今回の実装ではエンドポイント番号を固定したため、USB_RAW_IOCTL_EPS_INFO
の発行は省略しています。
二つ目は USB_RAW_EVENT_CONTROL
で、ホストからエンドポイント0に何らかのリクエストが届いた際に発行されます。
リクエストには、デバイス初期化のために必要な標準リクエストや、USBデバイスクラスに応じたクラスリクエスト、ベンダ独自のリクエストの三種類があります。
まずは、ホストがデバイス初期化ためにいくつかの標準リクエスト (GET_DESCRIPTOR
, SET_CONFIGURATION
等) を送信してきます。
リクエストにはOUTまたはINの(ホストから見た)方向が設定されており、OUTの場合はUSB_RAW_IOCTL_EP0_READ
を用いてデータの受信を行い、INの場合にはUSB_RAW_IOCTL_EP0_WRITE
を用いてホストにデータを送信します。
// EVENT_FETCH
struct usb_raw_event e;
e.event.type = 0;
e.event.length = sizeof(e.ctrl);
ioctl(fd, USB_RAW_IOCTL_EVENT_FETCH, &e.event);
// レスポンス用パケットの構造体を用意する
struct usb_packet_control pkt;
pkt.header.ep = 0;
pkt.header.flags = 0;
pkt.header.length = 0;
switch(e.event.type) {
case USB_RAW_EVENT_CONNECT:
// エンドポイント番号を動的に決める場合、ここで USB_RAW_IOCTL_EPS_INFO を用いてエンドポイントの情報を取得する
break;
case USB_RAW_EVENT_CONTROL:
// ここでコントロールパケットに対する処理を行い、必要な場合はpktにレスポンスのデータを詰める
// (省略)
if (e.ctrl.bRequestType & USB_DIR_IN) {
// ホストへレスポンスを返す
ioctl(fd, USB_RAW_IOCTL_EP0_WRITE, &pkt);
} else {
// ホストから追加のデータを受信する
ioctl(fd, USB_RAW_IOCTL_EP0_READ, &pkt);
}
break;
default:
break;
}
この USB_RAW_IOCTL_EVENT_FETCH
で取得できるイベントを知るため、USBデバイスの初期化の流れを紹介します。
USBデバイスの初期化の流れ
USBバスにUSBデバイスが接続されると、ホストがバスのリセットを実施し、エンドポイント0上でコントロール転送のみが利用できるようになります。
エンドポイント0上で、デバイスの初期化を行うためのリクエストがホストから送信されます。
リクエストは以下のような順序で送信されます。
- GET_DESCRIPTOR, Descriptor type: DEVICE
- デバイスデスクリプタの取得
- SET_ADDRESS
- デバイスアドレスの設定
- UDCが自動で処理を行うため、USB Raw Gadgetではこのリクエストに対するイベントは発生しません
- GET_DESCRIPTOR, Descriptor type: CONFIG
- コンフィギュレーションデスクリプタ、インターフェイスデスクリプタ、およびエンドポイントデスクリプタの取得
- GET_DESCRIPTOR, Descriptor type: STRING
- ストリングデスクリプタの取得 (デバイス名や製造者名などの文字列等が含まれまれるデスクリプタです)
- デバイス名を取得する必要が無いときは省略されます
- SET_INTERFACE
- 取得したインターフェイスの情報から、使用するインターフェイス番号を選びます
- SET_CONFIGURATION
- 使用する構成情報を指定し、USBデバイスの初期化を完了させます
- このリクエストが完了すると、追加のエンドポイントによる通信が可能になります
今回は既存のUSBモデムをエミュレートするため、各種デスクリプタは既存のデバイスからキャプチャーしたものを使用しました。
SET_CONFIGURATIONの際にデバイス側で行う操作
ホストがデバイスを正しく認識できた場合、通信の初期化を完了させるために SET_CONFIGURATION
リクエストが発行されます。
以降の通信では追加のエンドポイントを使用できるため、デバイス側ではエンドポイントの有効化を行う必要があります。
エンドポイントの有効化には USB_RAW_IOCTL_EP_ENABLE
を使います。
USB_RAW_IOCTL_EP_ENABLE
の戻り値は、後で対象のエンドポイントに対するREAD/WRITEを行う際に必要になります。
(おそらく、UDCで使用するインデックスだと思いますが未確認です)
また、コンフィギュレーション完了後はUSBデバイスの電流制限が変更になるため、USB_RAW_IOCTL_VBUS_DRAW
を用いて電流値を設定します。 (おそらくRaspberry Piでは何も起きません)
最後に、USB_RAW_IOCTL_CONFIGURE
を用いてコンフィギュレーションを完了させます。
// EP_ENABLE
struct usb_endpoint_descriptor desc_ep2_bulk_in = {/* ... */};
struct usb_endpoint_descriptor desc_ep2_bulk_out = {/* ... */};
int ep_bulk_in = ioctl(fd, USB_RAW_IOCTL_EP_ENABLE, &desc_ep2_bulk_in);
int ep_bulk_out = ioctl(fd, USB_RAW_IOCTL_EP_ENABLE, &desc_ep2_bulk_out);
// VBUS_DRAW
int bMaxPower = 30; // mA / 2 の値を代入する。60mAの場合は30を代入する.
ioctl(fd, USB_RAW_IOCTL_VBUS_DRAW, bMaxPower);
// CONFIGURE
ioctl(fd, USB_RAW_IOCTL_CONFIGURE, 0);
コンフィギュレーション完了後
コンフィギュレーションが完了した後は、エンドポイント0および追加のエンドポイントで通信ができます。
エンドポイント0では引き続き USB_RAW_IOCTL_EVENT_FETCH
を用いてイベントを受け取ります。
追加のエンドポイントでは、USB_RAW_IOCTL_EP_WRITE
、USB_RAW_IOCTL_EP_READ
を用いてデータパケットを発行できます。
USB Raw Gadgetでは現在のところノンブロッキングI/Oに対応していないため、非同期で複数のエンドポイントを使用する場合には、エンドポイントごとにスレッドを用意する必要があります。
struct usb_raw_bulk_io io;
// READ
io.inner.ep = ep_bulk_out;
io.inner.flags = 0;
io.inner.length = sizeof(io.data);
int ret = ioctl(fd, USB_RAW_IOCTL_EP_READ, &io); // 受信バイト数が返される
// WRITE
io.inner.ep = ep_bulk_in;
io.inner.flags = 0;
io.inner.length = data_length;
memcpy(&io.data, data, data_length);
ioctl(fd, USB_RAW_IOCTL_EP_WRITE, &io);
モデムエミュレータの実装
モデムのプロトコル解析の際にATコマンドが流れている様子が確認できていました。
このことから、エミュレーション対象のUSBモデムはソフトモデムではなくインテリジェントモデムだと考えられます。
インテリジェントモデムには、ATコマンドを用いて制御する「コマンドモード」と、接続後に相手とデータを送受信するための「オンラインモード」があります。
それぞれのモードについて簡単に説明をします。
コマンドモード
モデムの起動直後はコマンドモードになります。
コマンドモードはホストから送信されたATコマンドを処理するモードです。
ATコマンドは様々なコマンドがありますが、今回のエミュレーションに最低限必要なコマンドは ATA
、ATD
の二つでした。
未実装のコマンドに対しては、とりあえず OK
を返して動いたふりをするように実装しています。
ATD
コマンドは、指定した電話番号に対して発信して接続をするためのコマンドです。
このコマンドが成功すると、モデムは CONNECTED
と返答し、オンラインモードへ遷移します。
モデムエミュレータでは指定された電話番号をIPアドレスとして解釈し、ゲームソフト側から接続先IPを変更できるようにしています。
例えば、IPアドレス 192.168.100.1
のポート番号 12345
に接続をしたい場合、ゲーム内で接続先電話番号に 192-168-100-1#12345
を入力します。
すると、接続時に ATDT192-168-100-1#12345
というコマンドが発行されます。
このコマンドをパースしてIPアドレスとポート番号を取得し、ソケット接続を行います。
'ATA' コマンドは、着信に対して応答して接続するためのコマンドです。
外部から接続があった場合には、モデムからホストへ RING
と送信します。返答としてホストが ATA
コマンドを発行すると、接続が成立します。
このコマンドが成功すると、モデムは CONNECTED
と返答し、オンラインモードへ遷移します。
オンラインモード
オンラインモードでは、モデムはホストから受信したデータをそのまま接続先に転送します。
なので、今回はUSBホストから受信したデータをそのままソケット経由で相手に送信します。
また、相手からソケット経由で受信したデータはホストに送信します。
完成したモデムエミュレータの紹介
モデムエミュレータのプログラムはGitHub上で公開しています。
実際にモデムエミュレータを利用してインターネット経由でAC2AAのモデム対戦機能で安定して利用できるようになりました。
また、テストにご協力いただいた方によれば、AC3、AC3SLのモデム対戦機能でも同様に利用できたとのことでした。
なお,回線によってはゲームがかくつくような状態が生じることがありました。
これは、パケットロス発生時にTCPの再送待ちで通信がブロックされることが原因だと考えられるため、Reliable UDPを用いた再送処理の高速化による解消を目指しています。
AC2AA 遅延フレーム数の計測
テスターの方より、キー操作が画面に反映されるまでにかかったフレーム数を測定したデータ頂けたため紹介します。
画面 | 遅延フレーム数 |
---|---|
PS2起動直後の本体メニュー画面 | 0F (基準) |
AC2AA メニュー画面 | -2F |
戦闘画面 (スタンドアロン) | 2~3F |
戦闘画面 (i.Link対戦) | 5~6F |
戦闘画面 (モデム対戦, 専用モデム + 模擬交換機) | 10~11F |
戦闘画面 (モデム対戦, 専用モデム + VoIP) | 11~12F |
戦闘画面 (モデム対戦, 専用モデム + USBモデム終端方式) | 18F |
戦闘画面 (モデム対戦, モデムエミュレータ, RasPi Zero W 無線LAN) | 13~14F 7F (通信パラメータ調整後) |
戦闘画面 (モデム対戦, モデムエミュレータ, NanoPi NEO2 有線LAN) | 9F |
戦闘画面 (モデム対戦, モデムエミュレータ, RasPi 4 有線LAN) | 10F |
戦闘画面 (モデム対戦, モデムエミュレータ, RasPi Pico版) | 5~6F |
モデムエミュレータを使用した場合,Raspberry Pi 4と有線LANの組み合わせでは10フレームの遅延となっており、残念ながら専用モデムを用いた場合と大差は無いようでした。
一方で、後述するRaspberry Pi Pico版のモデムエミュレータは5~6フレーム程度と大幅に短縮でき、快適にプレイできるようになりました。
Raspberry Pi Pico互換マイコンボードへの移植
記事を執筆している2022年12月現在では、Raspberr Piは品薄となっており、入手が困難です。
また、入手できたとしても1万円程度の費用がかかることや、USB Raw Gadgetを用いたモデムエミュレータを使用するためにLinux上の操作も必要になることから、一般的なゲームプレイヤーが利用するにはハードルが高い可能性があります。
そこで、安価なEthertnet対応マイコンボードであるW5500-EVB-Picoへの移植を実施しました。
WIZnet W5500-EVB-PicoはRaspberry Pi Pico互換のマイコンボードです。
MPUであるRP2040に加え、WIZnet W5500というTCP/IPコントローラを搭載しています。
W5500-EVB-PicoはDigiKeyから購入することができ、価格は 1,470円 (執筆時現在) とお手頃な値段です。
なお、移植の際には、都合により既存のマイコン向けUSBスタックであるTinyUSBの利用ができなかったため、USBデバイスドライバを新たに実装しています。RP2040用USBデバイスドライバの実装についてはKLab Engineer Advent Calendar 2022 15日目の記事として投稿しています.
終わりに
今回は、USB Raw Gadgetを使ってUSBモデムをエミュレーションするプログラムを実装し、Raspberry Pi上で実行することで、PlayStation 2専用ゲーム「アーマード・コア2 アナザーエイジ」のモデム対戦をTCP/IPネットワーク上で利用できるようにしました。
「アーマード・コア2 アナザーエイジ」は2001年に発売されたゲームですが、21年の時を経た2022年にインターネット上で快適に対戦できるようになりました。
奇しくも先日には「アーマード・コア6」が発表されており、新作発売前にアーマード・コアシリーズの過去作をプレイしたいという方も増えているのではないかと思います。
AC2AA、AC3、AC3SLをプレイする際には、ぜひ今回作成したモデムエミュレータを使ったオンライン対戦も楽しんでいただけると嬉しいです。
謝辞
星月まほろ氏には、USBモデムエミュレータによる対戦テストへのご協力、通信パラメータのチューニング、および遅延フレーム数の計測結果についてご提供いただきました。ここに感謝の意を表します。
筆 智章氏には、USBモデムエミュレータよる対戦テストへのご協力、PS2専用モデムについての調査、および私が本件のモデム対戦機能に興味を持つきっかけを作っていただきました。ありがとうございました