アセットパック(追加アセットの読み込み)

概要

同梱されていない定義YAML・絵・表示文字列を、外部の配布物(アセットパック)から追加で 読み込む仕組みの設計ドキュメントです。配布物の形(ZIP)と、その中身をゲームがどう見つけるかを 決めます。

定義YAMLの文法そのものと、既存定義を差し替える patch 文法は GameElementDefinition.md(3節)が扱います。表示文字列の対応表の 形式は Localization.md が持ちます。

持ち込めるのはデータだけで、コードは持ち込めません(1節)。ただしこのゲームのデータは 振る舞いを含む——props・interactions・passives・recipes はすべて定義YAMLに書かれるので、 アセットパックは新しい物・新しい操作・新しいレシピを足せます。

WorldCodexの文法は増やしません。増えるのは配布物の中のディレクトリ構成と、絵の在処を実行時に 決める仕組みだけです。同梱ぶんは import.meta.glob がビルド時に一覧を作るので、パックぶんも同じ形の 一覧(本書では在庫表と呼ぶ)を配布物から作れれば、その先の仕組みは共通になります。

確定しているのは3.2節(pack.yaml の名乗り)と6.1.1節(捨てたぶんを画面から読める)と6.3節 (名前空間を設けない)だけで、他の記述は暫定です。【未実装】・【いつか】 の印が付いた節を除いて 実装済みです(src/asset-pack/、読み込みは src/loader/・ src/locale/Localization.ts・src/art/、検証は tests/asset-pack/)。節にならない未実装がもう1つ あり、絵に対する patch にあたる文法がまだありません(6節)。未決事項は7節に整理しています。

複数のパックを並べて読めますが、取得元は同梱のサンプルパック1つだけです (src/asset-pack/install.ts)。取得元を選ぶ画面はまだありません。

見本として、薬(毒薬・回復ポーション)のパックが入っています。型を足すだけでなく、同梱の土地の 探索結果へ patch で自分のアイテムを差し込んでいます(sample-pack/world-codex/potions.yaml)。 元のファイルは sample-pack/、配る形は public/sample-pack.zip で、npm run pack:sample が 固め直します(3.1節)。

読むかどうかはスタート画面の設定が決めます(StartScreen.md 画面構成 4、 既定は読まない)。パックは起動時に1回だけ入って以後不変(4節)なので、設定はゲームを始める前に しか変えられず、変えた値はページを読み込み直して初めて効きます。ビューア(src/codex-viewer/)は この設定によらず常に読みます——設定があるのはパックの物が世界に混ざるのを避けるためで、 ビューアは世界を始めずに定義を読むだけの道具だからです。

1. 持ち込めるのはデータだけ

コード(JavaScript)を含むパックには対応しません。 「MOD」と呼ばないのはこのためです。

ブラウザでZIP内のスクリプトを動かすこと自体は可能ですが、サンドボックスが無く、読み込んだ時点で ゲームと同じ権限を持ちます。加えて、バンドラの外にあるコードからは内部のクラスやモジュールが 公開契約になり、リファクタのたびに壊れます。データだけに限れば、パックが触れるのは WorldCodexの文法というすでに公開されている契約だけで済みます。

2. パックは1つのZIPで受け取る

パック1つ=ZIPファイル1つとし、個別ファイルをURLで拾いに行く形は採りません。

ZIPは末尾に中央ディレクトリ(全エントリの一覧)を持つため、在庫表が中身と一緒に届きます。 配布物をディレクトリのまま置く形だと、外部サーバではディレクトリ一覧を得る手段が無く、配る側が 一覧ファイルを別途用意して同期させることになります。それは同梱ぶんで手書きの一覧をやめた理由 (src/loader/loadWorldCodex.ts)をそのまま外部へ持ち出すだけです。

取得元がユーザーのローカルでも外部サーバでも、ArrayBuffer を得るところまでが違うだけで (<input type="file"> か fetch)、その先は同じ経路になります。今あるのは fetch の側だけです。

【いつか: パックの取得元】 ユーザーが選んだパックを入れる口は、同梱の見本1つで形が確かめられて いるので後から足します(Someday.md)。

取得そのものに失敗したときは、まだゲームを起動しません(src/asset-pack/install.ts)。ここは 「パックを読み込んだうえで一部が行えなかった」場合(6.1節)とは別で、パックを指定したのに何も 届いていない状態なので、設定の誤りとして扱います。

3. ZIPのトップが src/assets/ に相当する

ZIPの中身は src/assets/ 以下と同じ形にし、assets/ のような包む階層は設けません。 パックにはデータしか入らないので(1節)、src/assets/ がまさにその中身と同じ役割です。

中身 ZIP内の位置 同梱ぶんの位置
定義YAML world-codex/**/*.yaml src/assets/world-codex/
型の絵 objects/<型の絵の名前>.png src/assets/objects/
背景の絵 backgrounds/<持ち主>_<スロット名>_<用途>.png src/assets/backgrounds/
表示文字列 locale/<言語>.yaml src/assets/locale/

名前の規約(型の絵の名前・_multiply の接尾辞・背景の3語構成)は変わりません。読み手にとっては 「同梱の絵をどこに置くか」を知っていれば、パックの作り方も知っていることになります。同梱ぶんを そのままZIPに固め、pack.yaml(3.2節)を1つ足せば、それが1つのパックとして通ります。

絵は .png と .webp、定義は .yaml/.yml を読みます。これら以外のファイルは、入っていても 無視します。アイコン(icons/)・空(weather/)・シナリオ(scenarios/)はまだ読みません。

【いつか: 絵以外の資産】 音などを同じ在庫表に載せる話は、ゲーム本体が音を持ってからにします (Someday.md)。

3.1 同梱のサンプルは sample-pack/ を固めたもの

元のファイルはリポジトリに普通のファイルとして置き、ZIPはそれを固めた成果物です (npm run pack:sample → public/sample-pack.zip)。ZIPだけを置くと中身が差分に出ず、 置き方の規約を変えたときに黙って腐ります。

public/ はビルドが素通しで配る場所で、バンドルへ入る同梱データ(src/assets/)とは役割が 違います(CodeStructure.md 1節)。パックはゲームが取得しに行く配布物なので、 byte単位でそのまま配られる必要があります。

ZIPは誰がどの環境で固めても同じバイト列になります(時刻を書かない・並び順はパス順・テキストの 改行はLFへ揃える)。作業ツリーの改行はgitの取り出し方(core.autocrlf)で変わるので、読んだままを 入れると、中身を変えていないのに82KBが差分に出ます。何をどんなバイト列で入れるかは scripts/samplePackFiles.mjs が決め、packSample.mjs はZIPの形に組むだけです。

ZIPと sample-pack/ が食い違っていないかは tests/asset-pack/samplePack.test.ts が検査します。 期待するバイト列は同じ samplePackFiles.mjs から引きます——テストが自分でファイルを読み直すと、 中身が合っていても作業ツリーの改行しだいで落ちます。

3.2 パックは pack.yaml で自分の名前と版を名乗る【確定】

ZIPのトップに pack.yaml を1つ置き、パックの識別子と版をそこに書きます。どちらも省略できません。 同梱ぶんに対応するものが無い唯一のファイルで、3節の「ZIPのトップが src/assets/ に相当する」の 例外はこれだけです。

識別子はパックが自分で名乗り、ファイル名からは採りません。 ミラーやリネームでファイル名は変わり、 そのたびに同じパックが別物になります。セーブは記録した識別子でパックを照合する(6.4節)ので、 名前が動くとその瞬間にスロットが開けなくなります。

版は大小の順序を持ちません。 使い道は「同じ版か」の照合(6.4節)だけで、比べる用途が ありません。

出どころ: #656(pack.yaml に識別子と版を書かせ省略は認めない)と、同(版は文字列で順序を持たない)

3.3 pack.yaml の知らない欄はエラーにする

知らないキーはエラーにします(定義YAMLと同じ)。後から欄を足しても、それを書いていないパックは そのまま通ります。

表示用の名前の欄も、今は持ちません——画面に出すのは id と version そのものです。

3.4 同じ名乗りのパックは2つ入れられない

同じ id のパックを2つは入れられません(エラー)。出所の表示が一意でなくなり、どちらのパックの 話なのかが読めなくなります。同じパックの2つの版を並べて入れることもできません。

3.5 欄の名前と、識別子の見せ方

3.2節の識別子と版を、どう書かせてどこに出すかを決めたもので、覆すのに人間の判断は要りません。

欄の名前は id と version で、版はYAMLの文字列として書きます。

id: sample-pack
version: '1'

識別子が出てくるのは、定義の出所の表示(<id>:<ファイル名>)・エラーメッセージ・セーブの記録 (6.4節)です。

4. 絵の有無は在庫表に問い合わせる

「絵があれば使う」の判定は、常に在庫表への問い合わせで行い、読みに行って404かどうかでは 決めません。 この不変条件が、パック対応で最も壊れやすい箇所です。

現在の絵の解決は、import.meta.glob が作った一覧(ART_BY_NAME・BACKGROUND_ART)への 問い合わせだけでできています。

いずれも「無いのが普通」で、しかもカードやレーンの数だけ判定が走ります。ここを「読んでみて 404なら無い」に変えると、1画面あたり大量の404が出ます。

変えたのは在庫表の作り方だけです。ビルド時の定数から、起動時に同梱ぶんとパックぶんを重ねて 1回組み立てる形にしました(組み立て後は不変)。表の値はどちらも画像のURLで、パックぶんは ZIPのエントリから作るBlobのURLです。Blobは要求された絵の分だけ作り、同じ絵なら1つを使い回します。 どの絵をいつPhaserへ読ませるかは変わりません(起動時に読み切るぶんと、土地ごとの遅延ロード、 src/art/artFiles.ts)。

【いつか: パックの着脱】 遊んでいる途中で入れ替える道は、ページを読み込み直せば同じことができるので 後から足します(Someday.md)。

5. どのパックの絵かは、定義の出所で決まる

object_def の出所(どの配布物のどのファイルから読まれたか)を辿り、そのパックの在庫表を先に 見ます。 ローダーは既に定義の出所を持っています(重複を検出したときのエラーが出所を挙げる)。 出所は型が名乗る絵の名前に添えて運びます(<id>:<絵の名前>。同梱ぶんの名前に : は入らない ので、外せば同梱ぶんの名前になります)。

これにより、同じ識別子の絵を複数のパックが持っていても、その型を宣言した側の絵が使われます。 パックの中で見つからなければ同梱ぶんへ落ちるので、「定義だけを足して絵は同梱のものを使う」パックも そのまま書けます。

6. 差し替えは patch、絵に後勝ちは無い

絵にも定義と同じ規則を適用します。 定義は「同じ識別子が2箇所に現れたら常にエラー、意図的な 変更は patch 文法」(GameElementDefinition.md 3.4節)です。 絵だけ後勝ちにすると、パックを2つ入れた人の画面が、読み込み順という見えない要因で変わります。

型の絵は、同じ名前でも衝突しません。 どちらが出るかは定義の出所で決まる(5節)ので、 読み込み順によらず一意です。背景の絵は、同じ名前を2つのパックが持てません(エラー)。名前が 持ち主の識別子で始まる(3節)以上、同じ名前を持ち込むのは、宣言していない型の絵を配ったときだけです。

絵にはまだ patch にあたる文法がありません。他のパックや同梱ぶんの絵を、名前で指して差し替える ことはできません。

6.1 読み込みの失敗は、できたところまでで続ける

パックの中身はユーザーが書くもので、競合は起こって当たり前です——「標準の邪魔な機能を外す」 パックが2つ入れば、後の方は既に無いものを外そうとします。そこで止めるとプレイできなくなるため、 行えなかったぶんだけを捨てて先へ進みます。捨てた理由は記録に残します(src/loader/LoadReport.ts。 読める画面はまだ無く、当面はコンソールへ出ます。6.1.1 節)。

捨てる単位は2段です。

  1. patchの1操作(GameElementDefinition.md 3.4節)。remove の相手が 既に無い、add のキーが既にある、where が0件または2件以上——いずれも葉の編集なので、その操作 だけを捨てれば「そのパックがそこを頼まなかった」のと同じ状態になります。
  2. パック全体。ZIPが壊れている・YAMLの構文エラー・識別子が同梱と衝突・参照切れ、といった失敗は 操作単位で捨てられません。定義の一部だけを生かすと参照切れが残り、壊れ方が原因から遠い場所に 出ます。そのパックの定義を全部外して組み直せば、結果は必ず「同梱ぶん+無事なパック」という、 それ自体で筋の通った世界になります(届いた配布物そのものを読めないぶんは src/asset-pack/install.ts、定義を読んで落ちるぶんは src/loader/loadDefinitions.ts が外します)。

同梱ぶんの誤りは、今までどおり止めます。 ゲーム自身のバグで、外して続ける先がありません。 ここを緩めると、パックのせいに見える形で本体のバグが隠れます。

引き換えに、パックを書いた本人が自分の書き間違いに気付きにくくなります。記録を必ずコンソールへ 出すのはそのためです。

ここで守っているのは「パックの中身が期待どおりでなくても遊べる」ことだけです。セーブが指した パックが揃わない場合には及びません(6.4節)——揃わないまま開くと、同じシードから別の島が出ます。

6.1.1 捨てた定義と理由を、画面から読めるようにする【確定】【いつか: 読み込みの記録の画面】

読み込みで捨てた定義と、その理由の一覧を、画面から読めるようにします。 画面が1つ増えます。 今の時点では要らないので、入れるのは Someday.md の側です。

出どころ: #2404(読み込みで捨てた定義と理由を画面から読めるようにする。今は要らない。2026-09-29に回答)

6.2 複数のパックは、入れる人が並べた順に読む

読み込み順は取得元に並べた順で決まり、パックの側からは指定できません。 同梱ぶんが常に先頭で、 その後にパックが並んだ順で載ります。

順序が変えるのは並びだけです。同じ識別子が2度現れれば常にエラーで、後勝ちの上書きが無い(6節)ので、 どの順で読んでも出来上がる世界の中身は同じになります。順で変わるのは宣言順に振られるもの—— プロパティタグの並び、レシピ一覧の棚の並び(src/loader/WorldCodexYamlLoader.ts)といった、画面上の カテゴリの順だけです。

パックに順序を宣言させません。 2つのパックが互いに「自分が先」と言ったときの解決規則が要り、 その規則自体が新しい未決事項になります。並べるのは入れる人なので、入れる人が決めれば足ります。

patch(GameElementDefinition.md 3.4節)が他のパックの定義を指すときだけ、 指される側を先に置く必要があります。先に無ければ、その操作だけが捨てられます(6.1節)。

6.3 識別子に名前空間を設けない【確定】

パックが増えても、識別子はCodex全体で1つの平らな名前空間のままにします。 パックの id を 接頭辞に付けたり、パックごとに名前空間を分けたりはしません。

後から名前空間を入れると、既に書かれた識別子が全部変わります。patch は他のパックの定義を識別子で 指す(GameElementDefinition.md 3.4節)ので、接頭辞が付けば参照も全部 書き換えになり、しかもパックの id を変えただけで参照が切れるようになります。

衝突は隠さずエラーにします(6節)。同名を持ち込んだパックは丸ごと外れる(6.1節)ので、入れた人には どのパックを外せばよいかが分かります。パックを書く側は、自分の識別子に固有の接頭辞を付ける慣習で 避けられます——規約にはしません。 規約にすると、同梱ぶんの識別子にも接頭辞が要ることになります。

出どころ: #656(名前空間は設けず、ぶつかったらエラー)

6.4 パックの並びが違うセーブは開かない

セーブは作られたときのパックの並び(id と version を順序どおり)を記録し、今入っているものと 食い違うスロットは開きません。

パックは土地の型も生成の重みも足せて、島は定義から作られます(generateIsland)。つまり同じシード でも、入っているパックが違えば別の島になります。ワールド状態は保存せずシードから作り直す (SaveDataManagement.md ワールド状態の保存節)ので、食い違ったまま開くと、 続きのつもりで知らない島が出ます。

記録の欄はセーブの形式が持ちます(同 セーブデータのスキーマ節の assetPacks)。配ってしまってからでは 足せない欄です——記録の無いセーブは「同梱ぶんだけで作られた」としか読めず、パックを入れて遊んで いた人のスロットが、パック無しのセーブとして開かれます。まだ誰にも配っていない今のうちに足したので、 その代償は自分の手元のスロットだけで済みます(形式バージョン3までのセーブが1回そう読まれます)。

version まで含めて一致を見ます。版が上がれば定義が変わりうる以上、id だけの一致では「同じ島が出る」 とは言えません。引き換えに、パックを更新するとそれまでのスロットは開けなくなります。

棚(SaveDataManagement.md 棚節)は別です。パックが外れても、そのパックの 物の識別子を消しません。

7. 未決事項・今後の検討課題