📲

iPadに18禁ゲームをインストールする(ためのWebアプリを作る)

2024/05/06に公開

https://twitter.com/KichikuouWeb/status/1785245207413633293
これをどう作ったかの解説。

PWA

まず、上のスクリーンショットのアイコンはSafariの「ホーム画面に追加」で作られたものである。タップするとフレームなしのブラウザが立ち上がって、xsystem4(ゲームエンジン)のWebAssembly版が起動する。xsystem4のWebAssembly移植についてはこちらの記事に書いた。
https://zenn.dev/kichikuou/articles/a4d773c6d549a2#

この種のWebアプリはプログレッシブウェブアプリ (PWA)と呼ばれる。最近さっくり廃止されかかったりして、いつまで使えるかは少し心配でもあるが…。

iOS / iPadOS SafariにおけるPWA

SafariのPWA対応は他プラットフォームのChromium系ブラウザと大きく違っている点が一つあって、SafariとインストールされたPWAはストレージを共有しない。つまり、例えばSafariでログインしたユーザーがホーム画面にサイトをインストールしてそちらを開くと、ログイン状態ではなくなってしまう。

また同じPWAを複数回インストールした場合、インストールごとに個別のストレージが用意される。これを利用してアカウントを切り替える使い方ができる。

環境によって動作が違うという点では悩ましいが、今回は(Androidではxsystem4のネイティブアプリが使えるということもあり)Safariの仕様にべったり依存して作ることにした。ストレージ分離のおかげで複数のゲームをインストールでき、またホーム画面から削除するとそのゲームのストレージだけ解放されるためわかりやすい。

PWAとストレージ

昔のiOS Safariはオリジンごとに50MBとかしか保存できなかったと記憶しているが[1]、Safari 17.0でストレージポリシーが緩和されてWebアプリに大容量のデータを保存できるようになった。
https://www.webkit.org/blog/14403/updates-to-storage-policy/

System4ゲームの多くは1GB以上のデータを持つため、これらをインストールするにはSafari 17以降が必須ということになる。

また、navigator.storage.persist()を呼び出すと保存したデータをブラウザが自動的に(ユーザーの許可なしに)削除しないように指定できる。これが許可されるには条件があるが、ホーム画面にインストールされたPWAなら大丈夫なはず(未確認)。

XSystem4 Webインストーラ

こうした仕様を踏まえて、XSystem4 Webインストーラ Webサイトの実装を解説する。ソースコードは以下にある。

https://github.com/kichikuou/xsystem4-web

xsystem4実行エンジンを別にすれば小規模なコードベースなので、その気になれば全部読めると思う。

全体の流れ

ゲーム起動までの流れは以下のようになる。

  1. ユーザーがトップページ https://xsystem4-pwa.web.app/ を訪れる。
  2. ファイル選択ダイアログを使って、ユーザーが別途用意した(ゲームデータが入った)ZIPファイルを読み込む。
  3. ZIP中のデータを参照して、インストール用のWeb App Manifestを作る。ゲームデータのインストールはこの時点では行わないことに注意。なぜならここでゲームデータを保存しても、上記のストレージ分離によってインストールされたPWAからはアクセスできないからである。
  4. 「ホーム画面に追加」を行うよう促すメッセージを表示する。
  5. ユーザーがホーム画面に追加したアイコンをタップすると、新しいブラウザウィンドウが開き、先ほどと同じZIPファイルをもう一度選択するよう指示される。
  6. ユーザーが選んだZIP内のファイルをOPFSに展開する。ゲームデータはここでインストールされる。
  7. インストールが完了すると、OPFS内のファイルを使ってxsystem4の実行環境を設定し、ゲームを起動する。

次回以降ユーザーがホーム画面アイコンをタップすると、ステップ7から始まる。

以下、主要なステップをより詳しく見ていく。

ZIPファイルを読む

まずはユーザーが指定したZIPファイルを読む必要がある。JavaScriptからZIPを扱うライブラリはいくつもあるが、

  • 巨大なZIPファイルを扱える
  • ランダムアクセス可能
  • パス名の文字エンコーディングを指定可能(特にShift_JIS)

を全て満たすものが見つからなかったため、自前でZIPファイルをパースすることにした。Deflate圧縮の展開はブラウザ組み込みのDecompressionStream(Safariでは16.4からサポート)に任せてしまえるため、必要最低限の実装なら100行足らずで書けてしまう。

https://github.com/kichikuou/xsystem4-web/blob/0f2a03539e977acd0998209e99676fe824adab97/src/zip.ts

インストール用のWeb App Manifestを生成する

Web App ManifestはPWAインストール用のメタデータを記述したJSONファイル。ここでZIPファイルから取り出す必要があるのは、ゲームの名前とアイコン画像である。

System4ではゲームタイトルは設定ファイルSystem40.iniまたはAliceStart.iniに記述されているので、ZIPファイルからそれらを探して読めば良い。

アイコンに関しては2つのケースがある。

  • 拡張子.icoのファイルとして存在
  • Windows実行ファイルにリソースとして含まれる

後者の場合は実行ファイルのリソースセクションを読み込んで.icoを抽出している。

Safariは.ico形式を理解するので画像形式の変換は不要。data: URLにエンコードしてマニフェストのiconsに指定するか、<link rel="apple-touch-icon">要素に指定する。

マニフェストを生成したら、<link rel="manifest">要素としてDOMに追加する。これで「ホーム画面に追加」が実行された時、マニフェストの情報に従ってアイコンが作られる。

https://github.com/kichikuou/xsystem4-web/blob/0f2a03539e977acd0998209e99676fe824adab97/src/index.ts#L43-L75

ゲームデータのインストール

上で生成したマニフェストのstart_urlは、/play.htmlになっている。このページはゲームがインストールされていれば起動するが、PWAの初回起動でゲームデータがまだインストールされていない場合は/install.htmlに遷移する。

/install.htmlではユーザーにZIPをもう一度選択してもらい、ZIPの内容をOrigin Private File Systemに展開する。…と書くと単純な話のようだが、Safari特有の小さな落とし穴がいくつもある。

メインスレッドからはOPFSに書き込めない

SafariではFileSystemWritableFileStream未実装のため、ファイルに書き込むにはまずWorkerを作らなければならない。(読み込みはメインスレッドからも可能。)

ファイルからstream()で読み出すとOut of Memory

ファイルの(begin, end)の範囲を圧縮解除するには、ストリームAPIを使って

file.slice(begin, end).stream().pipeThrough(
    new DecompressionStream('deflate-raw'));

とすればいいように思える。しかしWebKitのBlob::stream()の実装はファイルを全力で読んでストリームにプッシュするので、圧縮されたデータが数百MBあるとOut of Memoryエラーを吐いてしまう。

仕方ないのでファイルを少しずつ読むReadableStreamを自分で書いた。まあ大した手間ではないが…。
https://github.com/kichikuou/xsystem4-web/blob/0f2a03539e977acd0998209e99676fe824adab97/src/worker/installer_worker.ts#L52-L73

ファイルの書き込みに失敗する

時々、OPFSへのファイルの書き込みが "Failed to write to file" や "Context has stopped" というエラーメッセージとともに失敗することがある。原因はよくわからないが、再度書き込むと成功することが多いのでリトライ処理を入れている。

ゲーム起動

OPFSにファイルを置いた後に/play.htmlに遷移するとゲームの起動処理に入る。具体的には以下のような流れとなる。

  1. xsystem4のEmscriptenモジュールをロード
  2. ゲームのファイルをOPFSからEmscriptenのファイルシステムに投入
  3. フォント等、他のリソースをEmscriptenファイルシステムに投入
  4. 準備ができたら、xsystem4のmain()を開始する

その他、このページにはxsystem4のWebAssemblyと協調して動作するJavaScriptが含まれる。例えばSystem4のゲームではセーブデータにコメントを書き込めるものがあるが、コメント入力画面で仮想キーボードが使えるよう透明な<input type="text">を画面に重ねるためのコードなどがある。

おわりに

独自仕様や制限が多くWebアプリには厳しい環境のiOS Safariだが、ストレージ制限の緩和など最近の改善によってこのようなPWAも作れるようになった。

外部のデータをホーム画面に「インストール」する仕組みは面白いと思うので、この方式のPWAを作る際の参考になれば幸いである。

脚注
  1. 今調べたところ、その後500MB上限の時代があったようだ ↩︎

Discussion