コイカツ技術メモ キャラカードの構造その1
はじめに
コイカツで作成したキャラクター、衣装、スタジオシーンのデータはpngとなって保存されます。 構成としては普通のpngのデータの中に拡張データを入れることによって実現しているようです。 今回はキャラカードのデータの中身をのぞいてみようかと思います。
pngの仕様
まずは土台になるpngの仕様から。ご存じの方は読み飛ばしてください。
pngファイルは、先頭の8バイトのシグネチャと、それに続く「チャンク」の列でできています。
89 50 4E 47 0D 0A 1A 0A ← シグネチャ("\x89PNG\r\n\x1a\n" 固定)
チャンクは「データ長→種別→データ本体→CRC」という構造の繰り返しで、ファイル全体との関係を図にするとこうなります。
今回はぶっちゃけ3つだけ覚えれば十分です。
| チャンク | 役割 |
|---|---|
| IHDR | 画像の幅・高さ・ビット深度など。必ず先頭 |
| IDAT | 圧縮された画像データ本体。複数個に分割されることが多い |
| IEND | 終端マーカー。データ長0 |
重要なのは「pngのパーサーはIEND以降を読み取らない」という点です。
IENDより後ろに何が書いてあっても、破損せずpngを表示することができます。
つまりIENDの後ろにバイナリを追記しても、画像としては完全に正常なpngのままになります。コイカツのカードはこの性質を利用してデータを埋め込んでいます。 元ファイルの構造さえ維持できていれば、ゲームから読み込むことが可能です。 pixivは元ファイルの構造を維持して表示することが可能なため、よく共有に利用されます。
中身を見てみる
というわけでどんな感じでデータが埋め込まれているのか、実際のカードを見てみましょう。 初期カードの一つ、「純真無垢」(羽田絵里)を覗いてみます。
このカードをバイナリエディタで開いて、まずはpng部分の終端にあたるIENDチャンクの周辺を見てみます。
00021338 57 E4 87 4C 00 00 00 00 49 45 4E 44 AE 42 60 82 |W..L....IEND.B`.|
00021348 64 00 00 00 12 E3 80 90 4B 6F 69 4B 61 74 75 43 |d.......KoiKatuC|
00021358 68 61 72 61 E3 80 91 05 30 2E 30 2E 30 15 16 01 |hara....0.0.0...|
00021368 00 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 |..PNG........IHD|
00021378 52 00 00 00 F0 00 00 01 40 08 02 00 00 00 0D 8A |R.......@.......|
ASCII欄にKoiKatuChara文字があり、コイカツのキャラカードである旨が書かれています。 順に意味を紐解くと...
49 45 4E 44 AE 42 60 82- IENDチャンクとそのCRC。pngの終端の意味を指します。
64 00 00 00- プロダクト番号で、どのゲームのカードかを意味してます。
- int32で100という意味で、エモクリで作られたカードはここが200になるらしいです。
- png本体のチャンク長はビッグエンディアンでしたが、拡張データの数値はすべてリトルエンディアンなので読む向きが逆になります。
12 E3 80 90 4B 6F ...- 文字列。先頭の12は長さプレフィックスで、中身はUTF-8の
【KoiKatuChara】。E3 80 90が【の3バイトです。 - この文字列はカード種別の判定に使われていて、コイカツサンシャインなら
【KoiKatuCharaSun】、衣装カードなら【KoiKatuClothes】になります。
- 文字列。先頭の12は長さプレフィックスで、中身はUTF-8の
05 30 2E 30 2E 30- データのバージョンを示すようです。
15 16 01 00- int32で
0x00011615 = 71,189。次に続くデータの長さです。
- int32で
89 50 4E 47 0D 0A 1A 0A- キャラの顔サムネのpngです。pngの中にpngを埋め込むってことしてます。
顔サムネの71,189バイトを読み飛ばした先はこうなっています。
0003297E B7 00 00 00 81 A7 6C 73 74 49 6E 66 6F 94 84 A4 |......lstInfo...|
0003298E 6E 61 6D 65 A6 43 75 73 74 6F 6D A7 76 65 72 73 |name.Custom.vers|
0003299E 69 6F 6E A5 30 2E 30 2E 30 A3 70 6F 73 00 A4 73 |ion.0.0.0.pos..s|
000329AE 69 7A 65 CD 0D 90 84 A4 6E 61 6D 65 AA 43 6F 6F |ize.....name.Coo|
000329BE 72 64 69 6E 61 74 65 A7 76 65 72 73 69 6F 6E A5 |rdinate.version.|
...
00032A2E CD FC 9A A4 73 69 7A 65 CD 02 ED 87 FF 00 00 00 |....size........|
B7 00 00 00- int32で
183。ヘッダーのバイト数を示しています。
- int32で
- 続く183バイトは、ASCII欄に
lstInfonameCustomversionpossize… と書いてあります。- ここはMessagePackが使われていて、デコードすると
A7は「7文字の文字列が続く」、CD 0D 90は「uint16の3472」といったように解釈できます。
- ここはMessagePackが使われていて、デコードすると
- その直後の
87 FF 00 00 00 00 00 00はint64で0xFF87 = 65,415。残りのデータ全体の長さを示してます。
これらを整理すると、カード全体像はこうなります。
BlockHeaderとブロック構造
拡張データの本体は「ブロック」という単位で管理されています。さっきの183バイトのヘッダーはこれの目次の役割をしています。デコードするとこうなります。
{
"lstInfo": [
{ "name": "Custom", "version": "0.0.0", "pos": 0, "size": 3472 },
{ "name": "Coordinate", "version": "0.0.0", "pos": 3472, "size": 60700 },
{ "name": "Parameter", "version": "0.0.5", "pos": 64172, "size": 494 },
{ "name": "Status", "version": "0.0.0", "pos": 64666, "size": 749 }
]
}
大きく分けて4つに分類されることがわかります。
それぞれ実際に中身をデコードして見ていきます。
Custom(外見データ)
Customブロックを覗いてみます。
内訳は以下のようになります。
| lump | サイズ | 中身 |
|---|---|---|
| 顔 | 1,451 bytes | shapeValueFace(スライダー52個)、headId、pupil(瞳)など |
| 体 | 780 bytes | shapeValueBody(スライダー44個)、bustSoftness、skinMainColor など |
| 髪 | 1,229 bytes | parts(髪パーツ4個: 後髪/前髪/横髪/エクステ)、glossId など |
顔
00032A41 AB 05 00 00 DE 00 26 A7 76 65 72 73 69 6F 6E A5 |......&.version.|
00032A51 30 2E 30 2E 32 AE 73 68 61 70 65 56 61 6C 75 65 |0.0.2.shapeValue|
00032A61 46 61 63 65 DC 00 34 CA 3E 69 78 D5 CA 3D FD F3 |Face..4.>ix..=..|
00032A71 B6 CA 00 00 00 00 CA 3F 42 10 84 CA 3E DF 3B 64 |.......?B...>.;d|
AB 05 00 00- int32で
1,451。 - プレフィックスでこの後1,451バイトのMessagePackがあることを示してます。
- int32で
DE 00 26- 「ここから“キー名と値”のペアが38組(0x26)続く」という印です。
- MessagePackではこれをmapと呼び、JSONの
{ }にあたります。以降もこの形が何度も出てきます。
- MessagePackではこれをmapと呼び、JSONの
- 1つ目のキーが
version、2つ目のキーがshapeValueFaceで顔のパラメータを埋め込んでます。
- 「ここから“キー名と値”のペアが38組(0x26)続く」という印です。
DC 00 34- 52要素(0x34)の配列。
CAはfloat32の印で、CA 3E 69 78 D5= 0.228、CA 3D FD F3 B6= 0.124、CA 00 00 00 00= 0.0、CA 3F 42 10 84= 0.758 …と続きます。- これはパラメータとしてそれぞれ 「顔全体の横幅(23)」、 「顔の上部前後(12)」、「顔の上部上下(0)」、「顔の上部サイズ(76)」 と表現されます。
この52個のスライダーが shapeValueFace、体タブの44個が shapeValueBody です。
体
構造は顔とまったく同じで、スライダー配列の少し先に今度は「色」が出てきます。
00032FF0 0C 03 00 00 DE 00 19 A7 76 65 72 73 69 6F 6E A5 |........version.|
00033000 30 2E 30 2E 32 AE 73 68 61 70 65 56 61 6C 75 65 |0.0.2.shapeValue|
00033010 42 6F 64 79 DC 00 2C CA 3E 87 6E A5 CA 3F 18 FB |Body..,.>.n..?..|
...
00033130 77 65 72 CA 3F 40 00 00 AD 73 6B 69 6E 4D 61 69 |wer.?@...skinMai|
00033140 6E 43 6F 6C 6F 72 94 CA 3F 80 00 00 CA 3F 67 CA |nColor..?....?g.|
00033150 E5 CA 3F 60 00 00 CA 3F 80 00 00 AC 73 6B 69 6E |..?`...?....skin|
0C 03 00 00- int32で
780。体のプレフィックスです。
- int32で
DE 00 19- ペアが25組(0x19)続く印。
versionは顔と同じ0.0.2です。
- ペアが25組(0x19)続く印。
AE shapeValueBody DC 00 2C- 44要素(0x2C)の配列。
- 先頭の
CA 3E 87 6E A5= 0.265 は「身長」を示します。
AD skinMainColor 94- 4要素の配列。
- それぞれ
CA 3F 80 00 00= 1.0、CA 3F 67 CA E5= 0.905、CA 3F 60 00 00= 0.875、CA 3F 80 00 00= 1.0で、肌色を示します。- RGBAのfloat4で
[1.0, 0.905, 0.875, 1.0]となります。
- RGBAのfloat4で
髪
髪は後ろ髪・前髪・横髪・エクステの4パーツの配列で、各パーツが id・色・長さを個別に持っています。
00033300 CD 04 00 00 84 A7 76 65 72 73 69 6F 6E A5 30 2E |......version.0.|
00033310 30 2E 34 A5 70 61 72 74 73 94 8A A2 69 64 0C A9 |0.4.parts...id..|
00033320 62 61 73 65 43 6F 6C 6F 72 94 CA 3F 3A 83 A8 CA |baseColor..?:...|
00033330 3F 11 AB E5 CA 3E DA 7C F9 CA 3F 80 00 00 AA 73 |?....>.|..?....s|
00033340 74 61 72 74 43 6F 6C 6F 72 94 CA 3F 20 EA 0F CA |tartColor..? ...|
00033350 3E F4 10 74 CA 3E EC C6 1E CA 3F 80 00 00 A8 65 |>..t.>....?....e|
00033360 6E 64 43 6F 6C 6F 72 94 CA 3F 20 EA 0F CA 3F 1B |ndColor..? ...?.|
00033370 2A D9 CA 3F 20 D5 0B CA 3F 80 00 00 A6 6C 65 6E |*..? ...?....len|
00033380 67 74 68 CA 00 00 00 00 A3 70 6F 73 93 CA 00 00 |gth......pos....|
CD 04 00 00- int32で
1,229。プレフィックスです。
- int32で
84- 「キー名と値のペアが4組続く」という印。顔の
DE 00 26と同じmapです。- 4組のキー名は
version/parts/kind/glossId。 versionは0.0.4。顔・体は0.0.2で別々のバージョン持ってます。(なんでだろう)
- 4組のキー名は
- 「キー名と値のペアが4組続く」という印。顔の
A5 70 61 72 74 73 94partsで値は4要素の配列。前から順に後ろ髪・前髪・横髪・エクステとなります。
8A- 1個目の要素(後ろ髪)の中身で、ペアが10組続きます。
A2 69 64 0CA2はfixstr(2文字の文字列)の印で、続く69 64はASCIIで"id"。- つまりキー
idとその値0C(12)のペアで、id=12が「ポニーテール」を意味します。
baseColor—CA 3F 3A 83 A8 …= [0.729, 0.569, 0.427, 1.0]- 髪色。茶髪です。
startColor/endColor- 根本→毛先のグラデーション色。
length—CA 00 00 00 00= 0.0- スライダーの値。
- 後ろ髪には長さスライダーが無いので0です。
pos/rot/scl- パーツごとの変形調整。
acsColor- 髪飾りの色(×4スロット)。
といった感じで髪のデータが構成されています。前髪や横髪なども同様で、4パーツそれぞれが自分のmapの中に同じ形のバイナリを1個ずつ持っています。
0x3331B A2 69 64 0C → 後ろ髪 id=12(ポニーテール)
0x33445 A2 69 64 01 → 前髪 id=1(ストレート)
0x3356F A2 69 64 00 → 横髪 id=0
0x33699 A2 69 64 00 → エクステ id=0
このような「プレフィックス」+「MessagePack」のブロックが顔・体・髪の3連続で入っているのがCustomブロックとなります。
Coordinate(衣装)
衣装スロットは7種あり、各要素は byte[](bin型)で、その中身がさらに入れ子のシリアライズデータになっています。
000337D1 97 C5 21 DE 4B 0E 00 00 85 A7 76 65 72 73 69 6F |..!.K.....versio|
000337E1 6E A5 30 2E 30 2E 31 A5 70 61 72 74 73 99 83 A2 |n.0.0.1.parts...|
000337F1 69 64 02 A9 63 6F 6C 6F 72 49 6E 66 6F 94 84 A9 |id..colorInfo...|
00033801 62 61 73 65 43 6F 6C 6F 72 94 CA 3F 66 66 68 CA |baseColor..?ffh.|
97- 7要素の配列。衣装7着を表します。
C5 21 DE- bin16。これもプレフィックスで「この後に0x21DE = 8,670バイトの生バイト列が続く」という意味です。
- 衣装1着はまるごとbyte[]として入れ子になっています。
4B 0E 00 00- そのbyte[]の中身の先頭。int32で
3,659。Customと同じ「プレフィックス」+「MessagePack」形式で、ここから服のデータが入っていることを示しています。
- そのbyte[]の中身の先頭。int32で
85- ペアが5組続く印。キー名は
version(0.0.1)/parts/subPartsId/hideBraOpt/hideShortsOpt。
- ペアが5組続く印。キー名は
A5 70 61 72 74 73 99partsは服の部位です。トップスから靴まで9個あります。
83- 各部位はペアが3組だけで、
id/colorInfo/emblemeId(ワッペン)です。 - 先頭の部位(トップス)は
A2 69 64 02=id=2。ジャケットタイプを示します。 colorInfo 94は4要素の配列 = 服の色スロット1〜4。各スロットは84(ペア4組)でbaseColor/pattern(柄id)/tiling(柄の細かさ)/patternColorを持ちます。
- 各部位はペアが3組だけで、
衣装1着分のバイナリ構成はこんな感じになります。
この後はアクセサリーになります。
00034624 80 12 00 00 82 A7 76 65 72 73 69 6F 6E A5 30 2E |......version.0.|
00034634 30 2E 32 A5 70 61 72 74 73 DC 00 14 86 A4 74 79 |0.2.parts.....ty|
00034644 70 65 79 A2 69 64 03 A9 70 61 72 65 6E 74 4B 65 |pey.id..parentKe|
80 12 00 00- int32で
4,736。 - アクセサリーの長さです。
- int32で
DC 00 14- 20要素(0x14)の配列。
- アクセサリー20スロットです。
86- 各スロットはペアが6組続きます。
A4 74 79 70 65 79— キーtype、値0x79 = 121(カテゴリ: 髪型アクセ)。A2 69 64 03—id = 3。ヘアピンにあたります。- この後に
parentKey("a_n_hair_pin" = 取り付けボーン名)、色、addMove(位置/回転/拡縮)などの各種設定が続きます。
アクセサリーは各スロットが種類と色に加えて位置・回転・拡縮の調整値まで持っています。
特徴として今まで位置や回転などのパラメーターは1/100のサイズで格納されていたのに対して、こちらは数字をそのまま入れてます。
スロット01のヘアピンをデコードすると type=121, id=3, parentKey="a_n_hair_pin"、調整値は位置 [-5.5, -4.3, -2.9]・回転 [21, 334, 342]・拡縮 [1.24, 1.06, 1.0]となっています。
アクセサリーの直後にある1バイトの00がenableMakeup = falseで、ここだけMessagePackを通さない生のboolです。あとは上の図のとおりで、1着目を使い切ると次のバイトから2着目が同じ構造で繰り返されます。
Parameter(キャラ設定)
名前、性別、性格、血液型、誕生日、H属性などのプロフィールです。ここもバイナリから見てみます。
000424ED DE 00 13 A7 76 65 72 73 69 6F 6E A5 30 2E 30 2E |....version.0.0.|
000424FD 35 A3 73 65 78 01 A8 6C 61 73 74 6E 61 6D 65 A6 |5.sex..lastname.|
0004250D E7 BE BD E7 94 B0 A9 66 69 72 73 74 6E 61 6D 65 |.......firstname|
0004251D A6 E7 B5 B5 E9 87 8C A8 6E 69 63 6B 6E 61 6D 65 |........nickname|
0004252D AC E3 81 AF E3 81 9F E3 81 88 E3 82 8A A8 63 61 |..............ca|
A3 73 65 78 01— キーsex、値1(女性)lastnameの後のA6 E7 BE BD E7 94 B0— 6バイトの文字列。UTF-8で読むと羽田です。- 同様に
E7 B5 B5 E9 87 8Cが絵里、E3 81 AF E3 81 9F E3 81 88 E3 82 8Aがはたえりとなります。
全体をデコードするとこうなります。
{ "lastname": "羽田", "firstname": "絵里", "nickname": "はたえり",
"sex": 1, "personality": 8, "bloodType": 1, "birthMonth": 7, "birthDay": 1, ... }
キャラ設定画面と見比べると、血液型b が bloodType: 1(a=0, b=1, …)、誕生日7月1日が birthMonth: 7, birthDay: 1、チアリーディング部が clubActivities: 3 と、選択式の項目は番号で入っているのが分かります。personality: 8 は性格「純真無垢」となります。
Status(状態)
現在着ている衣装番号(coordinateType)、服の着脱状態(clothesState)、表情パターン、まばたきの有無など、ランタイム寄りの状態が42キーほど入っています。
まとめ
- コイカツのカードは普通のpng+
IENDの後ろに追記された独自バイナリで構成されている。 - データ部分は「マーク文字列 → 顔サムネpng → BlockHeader(目次) → ブロック本体」の順。
- ブロックは Custom / Coordinate / Parameter / Status の4つで、シリアライズはMessagePackが使われている。スライダー値からアクセサリーの位置まで、キャラメイク画面の構成がそのまま入っている。
次回はこのへんのブロックの中身をもう少し掘るか、実際にパースするスクリプトを書いてみようかと思います。