https://twitter.com/KichikuouWeb/status/1785245207413633293
これをどう作ったかの解説。
まず、上のスクリーンショットのアイコンはSafariの「ホーム画面に追加」で作られたものである。タップするとフレームなしのブラウザが立ち上がって、xsystem4(ゲームエンジン)のWebAssembly版が起動する。xsystem4のWebAssembly移植についてはこちらの記事に書いた。
https://zenn.dev/kichikuou/articles/a4d773c6d549a2#
この種のWebアプリはプログレッシブウェブアプリ (PWA)と呼ばれる。最近さっくり廃止されかかったりして、いつまで使えるかは少し心配でもあるが…。
SafariのPWA対応は他プラットフォームのChromium系ブラウザと大きく違っている点が一つあって、SafariとインストールされたPWAはストレージを共有しない。つまり、例えばSafariでログインしたユーザーがホーム画面にサイトをインストールしてそちらを開くと、ログイン状態ではなくなってしまう。
また同じPWAを複数回インストールした場合、インストールごとに個別のストレージが用意される。これを利用してアカウントを切り替える使い方ができる。
環境によって動作が違うという点では悩ましいが、今回は(Androidではxsystem4のネイティブアプリが使えるということもあり)Safariの仕様にべったり依存して作ることにした。ストレージ分離のおかげで複数のゲームをインストールでき、またホーム画面から削除するとそのゲームのストレージだけ解放されるためわかりやすい。
昔の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インストーラ Webサイトの実装を解説する。ソースコードは以下にある。
https://github.com/kichikuou/xsystem4-web
xsystem4実行エンジンを別にすれば小規模なコードベースなので、その気になれば全部読めると思う。
ゲーム起動までの流れは以下のようになる。
次回以降ユーザーがホーム画面アイコンをタップすると、ステップ7から始まる。
以下、主要なステップをより詳しく見ていく。
まずはユーザーが指定したZIPファイルを読む必要がある。JavaScriptからZIPを扱うライブラリはいくつもあるが、
を全て満たすものが見つからなかったため、自前でZIPファイルをパースすることにした。Deflate圧縮の展開はブラウザ組み込みのDecompressionStream(Safariでは16.4からサポート)に任せてしまえるため、必要最低限の実装なら100行足らずで書けてしまう。
https://github.com/kichikuou/xsystem4-web/blob/0f2a03539e977acd0998209e99676fe824adab97/src/zip.ts
Web App ManifestはPWAインストール用のメタデータを記述したJSONファイル。ここでZIPファイルから取り出す必要があるのは、ゲームの名前とアイコン画像である。
System4ではゲームタイトルは設定ファイルSystem40.ini
またはAliceStart.ini
に記述されているので、ZIPファイルからそれらを探して読めば良い。
アイコンに関しては2つのケースがある。
.ico
のファイルとして存在後者の場合は実行ファイルのリソースセクションを読み込んで.ico
を抽出している。
Safariは.ico
形式を理解するので画像形式の変換は不要。data: URLにエンコードしてマニフェストのicons
に指定するか、<link rel="apple-touch-icon">
要素に指定する。
マニフェストを生成したら、<link rel="manifest">
要素としてDOMに追加する。これで「ホーム画面に追加」が実行された時、マニフェストの情報に従ってアイコンが作られる。
上で生成したマニフェストのstart_url
は、/play.html
になっている。このページはゲームがインストールされていれば起動するが、PWAの初回起動でゲームデータがまだインストールされていない場合は/install.html
に遷移する。
/install.html
ではユーザーにZIPをもう一度選択してもらい、ZIPの内容をOrigin Private File Systemに展開する。…と書くと単純な話のようだが、Safari特有の小さな落とし穴がいくつもある。
SafariではFileSystemWritableFileStream
が未実装のため、ファイルに書き込むにはまずWorkerを作らなければならない。(読み込みはメインスレッドからも可能。)
stream()
で読み出すとOut of Memoryファイルの(begin, end)
の範囲を圧縮解除するには、ストリームAPIを使って
file.slice(begin, end).stream().pipeThrough(newDecompressionStream('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
に遷移するとゲームの起動処理に入る。具体的には以下のような流れとなる。
main()
を開始するその他、このページにはxsystem4のWebAssemblyと協調して動作するJavaScriptが含まれる。例えばSystem4のゲームではセーブデータにコメントを書き込めるものがあるが、コメント入力画面で仮想キーボードが使えるよう透明な<input type="text">
を画面に重ねるためのコードなどがある。
独自仕様や制限が多くWebアプリには厳しい環境のiOS Safariだが、ストレージ制限の緩和など最近の改善によってこのようなPWAも作れるようになった。
外部のデータをホーム画面に「インストール」する仕組みは面白いと思うので、この方式のPWAを作る際の参考になれば幸いである。
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。