本文へスキップ
コイカツ技術置き場

コイカツ技術メモ キャラカードの構造その2

この記事をシェア X はてブ

はじめに

前回ではキャラカードの中身が Custom / Coordinate / Parameter / Status の4ブロックで構成されていることがわかりました。

今回はmodのデータがどう保存されるかを確認します。modが入った環境でカードを保存すると、5つ目のブロック「KKEx」 が追加されます。これはExtendedSaveというプラグインが管理する「mod専用の保存領域」で、どのmodのデータもすべてここに集約される仕様になっています。

これを確かめるため、手元の入華のカードの中身をのぞいてみようかと思います。

Koikatu-2026-08-30-19-23-09-UI

全体像はこうなっています。

咲宮入華.png(5,644,567 bytes) png本体(カードの表絵) 156,520 bytes ← ここから拡張データ ヘッダ(プロダクト番号・マーク文字列・バージョン) 45 bytes 顔サムネpng 154,482 bytes BlockHeader(目次: 5エントリ・229 bytes) Custom / Coordinate / Parameter / Status (バニラの4ブロック。前回と同じ構造) 計 78,235 bytes KKEx(mod拡張データ) 18プラグインの寄せ書き。今回の主役 5,255,056 bytes 容量の内訳: KKEx 93.1% 表絵+顔サムネ+バニラデータ ≈ 7%
入華カードのバイナリ構成(実測値)

ヘッダー構造

ヘッダ部分の構造は前回とまったく同じです。変わるのは目次の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の長さです。
  • 95
    • lstInfoは5要素の配列。
    • 前回の4要素から1つ増えており、modの項目が追加されています。
  • A4 4B 4B 45 78
    • 今回増えた文字列 KKEx。
    • これが5つ目のブロックです。
  • A3 70 6F 73 CE 00 01 31 9B
    • pos = uint32で78,235。
    • 前回のposはuint16(CD)で足りていましたが、今回は大きいのでuint32(CE)になっています。
  • A4 73 69 7A 65 CE 00 50 2F 90
    • size = 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。
  • 値は必ず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の名前がそのまま入っています。

このように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あります。

テクスチャ画像はこんな感じです。

ピンクのストライプ地に白いマーガレットの花が散ったショーツのテクスチャ。UV展開に沿ってT字型に配置され、市松模様の部分は透明
ID 97の中身。水着コーデのパンツ(`cf_shorts_oband_m` の `MainTex`)に貼られている花柄ショーツ。市松の部分は透明

こんな感じで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 — GUID KSOX。
  • 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ファイル本体です。

KSOXが持っているのはpng2枚と、それをどのコーデで使うかの Lookup です。2枚を取り出すとこうなっています。

顔用の肌オーバーレイ。頬の紅潮、鼻先の赤み、唇の影だけが描かれ、残りは透明
`_TextureID_9812`(1024×1024)。顔用。頬・鼻先・唇の赤みだけを描いた半透明の一枚
体用の肌オーバーレイ。鎖骨、へそ、陰部、膝、手のひら、足裏に薄い赤みが置かれ、残りは透明
`_TextureID_5326`(2048×2048)。体用。へそ・膝・手足の裏などに赤みを足す

どちらも肌テクスチャの上に重ねる画像です。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 }
  • ObjectType 1は服、CoordinateIndex 3は水着コーデ、Slot 3はパンツ枠。
  • 水着コーデのパンツのコーデである cf_shorts_oband_m(ローライズビキニ)の MainTex に、ID 97のpngを貼るということをしてます。
Koikatu-2026-09-01-12-02-30-UI

バイナリで見るとこうなっています。

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枚で済みます。

今回のカードだと入華の髪飾りが該当します。

Koikatu-2026-09-01-13-26-46-UI Koikatu-2026-09-01-13-26-59-UI

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 headId 01
    • 本体に書かれているのは 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の番号か」を添えて、ロード時に環境の番号へ貼り替える。

次回はシーンデータを見てみます。