コイカツ技術メモ キャラカードの構造その2
はじめに
前回ではキャラカードの中身が Custom / Coordinate / Parameter / Status の4ブロックで構成されていることがわかりました。
今回はmodのデータがどう保存されるかを確認します。modが入った環境でカードを保存すると、5つ目のブロック「KKEx」 が追加されます。これはExtendedSaveというプラグインが管理する「mod専用の保存領域」で、どのmodのデータもすべてここに集約される仕様になっています。
これを確かめるため、手元の入華のカードの中身をのぞいてみようかと思います。
全体像はこうなっています。
ヘッダー構造
ヘッダ部分の構造は前回とまったく同じです。変わるのは目次のBlockHeaderで、ExtendedSaveが保存時に KKEx という名前のエントリを1つ追記します。名前は常に KKEx 固定、version はExtendedSaveのフォーマット版数(現行 3)、pos / size はカードごとに変わります。
実例で見てみます。
0004BEFB E5 00 00 00 81 A7 6C 73 74 49 6E 66 6F 95 84 A4 |......lstInfo...|
0004BF0B 6E 61 6D 65 A4 4B 4B 45 78 A7 76 65 72 73 69 6F |name.KKEx.versio|
0004BF1B 6E A1 33 A3 70 6F 73 CE 00 01 31 9B A4 73 69 7A |n.3.pos...1..siz|
0004BF2B 65 CE 00 50 2F 90 84 A4 6E 61 6D 65 A6 43 75 73 |e..P/...name.Cus|
0004BF3B 74 6F 6D A7 76 65 72 73 69 6F 6E A5 30 2E 30 2E |tom.version.0.0.|
0004BF4B 30 A3 70 6F 73 00 A4 73 69 7A 65 CD 0E 54 84 A4 |0.pos..size..T..|
E5 00 00 00- int32で
229。 - BlockHeaderの長さです。
- int32で
95lstInfoは5要素の配列。- 前回の4要素から1つ増えており、modの項目が追加されています。
A4 4B 4B 45 78- 今回増えた文字列
KKEx。 - これが5つ目のブロックです。
- 今回増えた文字列
A3 70 6F 73 CE 00 01 31 9Bpos= uint32で78,235。- 前回のposはuint16(
CD)で足りていましたが、今回は大きいのでuint32(CE)になっています。
A4 73 69 7A 65 CE 00 50 2F 90size= 5,255,056。カード5.6MBのうち5.3MBがこのブロックです。
デコードして整理するとこうなります。
{
"lstInfo": [
{ "name": "KKEx", "version": "3", "pos": 78235, "size": 5255056 },
{ "name": "Custom", "version": "0.0.0", "pos": 0, "size": 3668 },
{ "name": "Coordinate", "version": "0.0.0", "pos": 3668, "size": 73226 },
{ "name": "Parameter", "version": "0.0.5", "pos": 76894, "size": 574 },
{ "name": "Status", "version": "0.0.0", "pos": 77468, "size": 767 }
]
}
やっていることは目次に1行足して、末尾にデータを置くだけです。目次上はKKExが先頭に宣言されていますが、pos = 78,235 が示すとおり実データはバニラ4ブロックの後ろにあります。
既存ブロックには一切手を触れないので、バニラの読み込み処理は壊れません。なのでmodなしの環境でも読み取り自体は可能だったりします。(異形になるけど)
中身
KKExの中身は、MessagePackのmapが1個あるだけです。
{ "プラグインのGUID": [バージョン, データのmap], ... }
- キー名はmodプラグインのGUID文字列
- zipmodを読み取るSideloader UARの場合は
com.bepis.sideloader.universalautoresolver。 - マテリアルエディターの場合は
com.deathweasel.bepinex.materialeditor。
- zipmodを読み取るSideloader UARの場合は
- 値は必ず2要素の配列
[version, data]になる。 dataの中身は各プラグインの自由。ロード時にGUIDを見て各modプラグインに解釈を委ねる。
各modが自分のGUIDで好きなデータをぶら下げるだけなので、何がいくつ入るかはカードごとに違います。その合計がKKExのサイズです。
実例の入口を見てみます。
0005F187 DE 00 12 D9 2A 63 6F 6D 2E 62 65 70 69 73 2E 73 |....*com.bepis.s|
0005F197 69 64 65 6C 6F 61 64 65 72 2E 75 6E 69 76 65 72 |ideloader.univer|
0005F1A7 73 61 6C 61 75 74 6F 72 65 73 6F 6C 76 65 72 92 |salautoresolver.|
0005F1B7 00 81 A4 69 6E 66 6F DC 00 59 C4 73 88 A5 4D 6F |...info..Y.s..Mo|
0005F1C7 64 49 44 AB 74 75 72 69 6D 65 5F 66 61 63 65 A4 |dID.turime_face.|
0005F1D7 53 6C 6F 74 01 A9 4C 6F 63 61 6C 53 6C 6F 74 CE |Slot..LocalSlot.|
0005F1E7 05 F6 D5 70 A8 50 72 6F 70 65 72 74 79 B2 43 68 |...p.Property.Ch|
DE 00 12- MessagePackのmapの中身が18組(0x12)あるという意味。
- modの項目が18個ある。
D9 2A 63 6F 6D ...D9はstr8(1バイト長付き文字列)という意味。- 0x2A = 42文字。中身は
com.bepis.sideloader.universalautoresolverで、Sideloaderを意味してます。
92- 2要素の配列。
[バージョン, データ]の組み合わせです。 00(バージョン0)に続いて81 A4 69 6E 66 6F=infoのペア1組だけのmap、DC 00 59= 89要素の配列…とSideloaderの情報が続きます。- ASCII欄に
turime_faceと読めるのがその1件目。使っているmodの名前がそのまま入っています。
- 2要素の配列。
このようにmodの情報が18個格納されているのですが、内訳は以下のようになります。
| プラグイン | サイズ | 中身 |
|---|---|---|
| MaterialEditor(以下ME) | 3.7MB | テクスチャ辞書とマテリアルのプロパティ差分 |
| KSOX (Skin Overlay Mod) | 845KB | 肌のオーバーレイテクスチャ |
| KCOX (Clothes Overlay Mod) | 661KB | 服のオーバーレイテクスチャ |
| Pushup | 31KB | ブラ・トップス着用時の胸の変形設定 |
| Sideloader UAR | 16KB | modのGUID→ゲーム内IDの解決テーブル |
| moreAccessories | 7.7KB | 21個目以降のアクセサリー |
| ほか12プラグイン | 計5.5KB | ボーン編集、髪アクセ設定、作者情報等 |
MEが3.7MB、他にも服のテクスチャが合計で1MB以上あり、この3つが容量の大半を占めています。 今回は例としてSideloaderとMEとオーバレイを見てみます。
MEのテクスチャ
MEはキャラのテクスチャの差し替えや色や光沢を調整できるmodプラグインで、キャラクリにおける必須プラグインと呼べます。
MEは差し替えたテクスチャを TextureDictionary というフィールドに「テクスチャID → pngのバイト列」の辞書として保存します。pngは圧縮されません。
実例カードでMEのエントリまでシークするとこうなっています。
001D5E10 C2 27 C2 28 C2 29 C2 2A C2 2B C2 2C C2 2D C2 D9 |.'.(.).*.+.,.-..|
001D5E20 26 63 6F 6D 2E 64 65 61 74 68 77 65 61 73 65 6C |&com.deathweasel|
001D5E30 2E 62 65 70 69 6E 65 78 2E 6D 61 74 65 72 69 61 |.bepinex.materia|
001D5E40 6C 65 64 69 74 6F 72 92 00 8A B1 54 65 78 74 75 |leditor....Textu|
001D5E50 72 65 44 69 63 74 69 6F 6E 61 72 79 C6 00 37 7D |reDictionary..7}|
001D5E60 03 8F 61 C6 00 0A E6 90 89 50 4E 47 0D 0A 1A 0A |..a......PNG....|
C2 27 C2 28 ...- 前のプラグインのデータの尻尾。
D9 26 com.deathweasel.bepinex.materialeditor- MEのGUIDキー。
92 00 8A[バージョン, データ]の組み合わせ。- バージョン0、データはペアが10組続くmapで、1つ目のキー名が
TextureDictionary。
C6 00 37 7D 03- データの長さ。
C6はbin32(4バイト長付きのバイト列)。長さは 0x00377D03 = 3,636,483バイト。
8F- データの先頭(0x8F = 15)。
- ペアが15組続くmap。これがテクスチャ辞書本体で、キー名はテクスチャID、値は画像のバイト列です。
61 C6 00 0A E6 90 89 50 4E 47 ...- 1つ目のエントリ。キー
0x61(ID 97)。 C6 00 0A E6 90はbin32で長さ714,384バイト。続く89 50 4E 47からその長さ分がpngファイル本体です。IHDRを読むと00 00 08 00= 2048×2048のテクスチャとなります。- ちなみにこれ1枚で700KBあります。
- 1つ目のエントリ。キー
テクスチャ画像はこんな感じです。
こんな感じでmodの画像データが入っていくと、テクスチャが2枚あるだけで1MBは余裕で超えます。
mod入りカードの容量が肥大化する要因は大体これです。
オーバレイのテクスチャ
画像を扱うmodは軒並み同じ実装です。オーバーレイ系(KSOX/KCOX)のエントリも覗いてみます。
00064E60 6E 61 6C 41 63 63 65 73 73 6F 72 69 65 73 3E A4 |nalAccessories>.|
00064E70 4B 53 4F 58 92 02 83 AF 5F 54 65 78 74 75 72 65 |KSOX...._Texture|
00064E80 49 44 5F 39 38 31 32 C6 00 06 20 E2 89 50 4E 47 |ID_9812... ..PNG|
A4 4B 53 4F 58— GUIDKSOX。92 02 83—[バージョン2, ペア3組のmap]。AF 5F 54 65 ...- キー
_TextureID_9812。
- キー
C6 00 06 20 E2- bin32で長さ401,634バイト。続く
89 50 4E 47からその長さ分がpngファイル本体です。
- bin32で長さ401,634バイト。続く
KSOXが持っているのはpng2枚と、それをどのコーデで使うかの Lookup です。2枚を取り出すとこうなっています。
どちらも肌テクスチャの上に重ねる画像です。Lookup の中身は {コーデ番号: {4: 9812, 1: 5326}} の7組で、全コーデが同じ2枚を指していました。
テクスチャの残り
テクスチャがどこの何に適用するかも残りのバイト列に書かれています。
| フィールド | サイズ | 件数 | 中身 |
|---|---|---|---|
| MaterialFloatPropertyList | 18,418 | 161 | 数値プロパティの変更(リムライト強度など) |
| MaterialShaderList | 12,672 | 73 | シェーダーの差し替え |
| MaterialColorPropertyList | 12,200 | 82 | 色プロパティの変更(影色など) |
| MaterialTexturePropertyList | 5,097 | 32 | テクスチャの貼り先とUV調整 |
| RendererPropertyList | 2,870 | 27 | レンダラーの表示/非表示など |
| ほか4フィールド | 0 | 0 | 未使用(nil) |
どのレコードも Value と ValueOriginal を対で持っていて、変更後の値と一緒に元の値も保存されています。
さっきの花柄のテクスチャをどこで使っているのかはMaterialTexturePropertyList を見るとわかります。
{ "ObjectType": 1, "CoordinateIndex": 3, "Slot": 3, "MaterialName": "cf_shorts_oband_m", "Property": "MainTex", "TexID": 97 }
ObjectType1は服、CoordinateIndex3は水着コーデ、Slot3はパンツ枠。- 水着コーデのパンツのコーデである
cf_shorts_oband_m(ローライズビキニ)のMainTexに、ID 97のpngを貼るということをしてます。
バイナリで見るとこうなっています。
005568BE 4E 61 6D 65 B1 63 66 5F 73 68 6F 72 74 73 5F 6F |Name.cf_shorts_o|
005568CE 62 61 6E 64 5F 6D A8 50 72 6F 70 65 72 74 79 A7 |band_m.Property.|
005568DE 4D 61 69 6E 54 65 78 A5 54 65 78 49 44 61 A6 4F |MainTex.TexIDa.O|
A5 54 65 78 49 44 61- キー
TexID、値0x61= 97。 - テクスチャ辞書の先頭にあったキー
61と同じ値で、ここで辞書と貼り先が結ばれます。
- キー
pngのバイナリが一致すれば重複扱いされるので、同じpngを複数の貼り先に使っても辞書側は1枚で済みます。
今回のカードだと入華の髪飾りが該当します。
Sideloader UAR
Sideloader UARはmod のパーツ番号を環境ごとに翻訳する仕組みです。
ゲーム本体は服や髪を「カテゴリ+番号」で持っています(例: 頭部 = カテゴリ100 の 1番)。バニラの番号は固定ですが、zipmod のパーツは入れている mod の数や順序で変わるので、そのままカードに書くと別のPCでは別のパーツを指してしまいます。
そこでUARは、保存時にカード本体(バニラのブロック)には mod内の元の番号 を書き、KKExの表に「その項目のその番号は、どのmodのものか」を添えます。ロード時はこの表を引いて、今の環境の番号に貼り替えます。
例えばこのカードの頭部は [i]turime_face v2.2.zipmod というmodの1番に割り当てられていて、こちらのPCでの環境では実行時に100062576番が割り振られていました。
| 番号の割り振り方 | 番号 | 環境で変わるか | |
|---|---|---|---|
| mod内の番号(Slot) | zipmod内のlist csv | 1 | 変わらない |
| 実行時の番号(LocalSlot) | 起動時にSideloaderが割り振り | 100062576 | 変わる |
カード本体とKKExの2箇所を並べて見ます。まずCustomブロック側の headId です。
0004C118 00 A6 68 65 61 64 49 64 01 A6 73 6B 69 6E 49 64 |..headId..skinId|
A6 headId01- 本体に書かれているのは
1だけ。どのmodの1番かは書かれていません。
- 本体に書かれているのは
次にKKEx側。「中身」の節で turime_face と読めていた1件目です。
0005F1C3 88 A5 4D 6F 64 49 44 AB 74 75 72 69 6D 65 5F 66 |..ModID.turime_f|
0005F1D3 61 63 65 A4 53 6C 6F 74 01 A9 4C 6F 63 61 6C 53 |ace.Slot..LocalS|
0005F1E3 6C 6F 74 CE 05 F6 D5 70 A8 50 72 6F 70 65 72 74 |lot....p.Propert|
0005F1F3 79 B2 43 68 61 46 69 6C 65 46 61 63 65 2E 68 65 |y.ChaFileFace.he|
ModID=turime_face、Slot=1、Property=ChaFileFace.headId- 「
headIdの 1 番はturime_faceの 1 番」という1行です。
- 「
本体の headId = 1 とこのレコードが headId と 1 で結ばれていて、ロード時はこの表から今の環境の番号を求めて貼り替えます。
このカードの表は89件・29 modで、うち55件が頭・顔アクセサリー(カテゴリ122/123)でした。髪をアクセサリーで組んでいるカードは、それだけ翻訳表も長くなります。
まとめ
- mod環境で保存したカードには5つ目のブロック KKEx が追加される。
- 中身は「プラグインのGUID →
[バージョン, データ]」の辞書。データ自体は各自のmodに解釈を任せているため形式が様々。 - MEもオーバーレイ系も、テクスチャはpngのまま埋め込まれる。
- MEはpng本体を
TextureDictionaryに貼り先や色・シェーダーの変更を持つ。 - Sideloader UARはIDの翻訳表。本体にはmod内の番号だけを書き、KKExに「どのmodの番号か」を添えて、ロード時に環境の番号へ貼り替える。
次回はシーンデータを見てみます。