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

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

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

はじめに

コイカツで作成したキャラクター、衣装、スタジオシーンのデータはpngとなって保存されます。 構成としては普通のpngのデータの中に拡張データを入れることによって実現しているようです。 今回はキャラカードのデータの中身をのぞいてみようかと思います。

pngの仕様

まずは土台になるpngの仕様から。ご存じの方は読み飛ばしてください。

pngファイルは、先頭の8バイトのシグネチャと、それに続く「チャンク」の列でできています。

89 50 4E 47 0D 0A 1A 0A   ← シグネチャ("\x89PNG\r\n\x1a\n" 固定)

チャンクは「データ長→種別→データ本体→CRC」という構造の繰り返しで、ファイル全体との関係を図にするとこうなります。

pngファイル全体 シグネチャ(8 bytes 固定) IHDR チャンク(画像情報) IDAT チャンク × n (圧縮された画像データ) IEND チャンク(終端) ← 画像ビューアはここで読み終わる IEND より後ろは自由領域 コイカツはここに キャラデータを追記する チャンク1個の中身 データ長(4 bytes・BE) チャンク種別(ASCII 4文字) データ本体(データ長ぶん) CRC32(4 bytes)
pngファイルの構造

今回はぶっちゃけ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】になります。
  • 05 30 2E 30 2E 30
    • データのバージョンを示すようです。
  • 15 16 01 00
    • int32で 0x00011615 = 71,189。次に続くデータの長さです。
  • 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。ヘッダーのバイト数を示しています。
  • 続く183バイトは、ASCII欄に lstInfo name Custom version pos size … と書いてあります。
    • ここはMessagePackが使われていて、デコードするとA7 は「7文字の文字列が続く」、CD 0D 90 は「uint16の3472」といったように解釈できます。
  • その直後の 87 FF 00 00 00 00 00 00はint64で0xFF87 = 65,415。残りのデータ全体の長さを示してます。

これらを整理すると、カード全体像はこうなります。

純真無垢.png(272,840 bytes) png本体(カードの表絵) IHDR 〜 IDAT×17 〜 IEND 136,008 bytes ← ここから拡張データ int32 loadProductNo = 100 string マーク文字列 = "【KoiKatuChara】" string バージョン = "0.0.0" int32 顔サムネpngの長さ = 71,189 byte[] 顔サムネpng (Makerのリスト用の顔画像。pngがもう1枚丸ごと入っている) 71,189 bytes int32 BlockHeaderの長さ = 183 byte[] BlockHeader(MessagePack製の目次) int64 ブロック領域全体の長さ = 65,415 byte[] ブロック本体(この先頭が pos の基準点) Custom / Coordinate / Parameter / Status 65,415 bytes
純真無垢.png のバイナリ構成(実測値)

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があることを示してます。
  • DE 00 26
    • 「ここから“キー名と値”のペアが38組(0x26)続く」という印です。
      • MessagePackではこれをmapと呼び、JSONの { } にあたります。以降もこの形が何度も出てきます。
    • 1つ目のキーがversion、2つ目のキーが shapeValueFaceで顔のパラメータを埋め込んでます。
  • 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)」 と表現されます。
キャラメイクの顔タブ。顔全体の横幅23、顔の上部前後12、顔の上部上下0、顔の上部サイズ76、顔の下部前後44と変形スライダーが並ぶ 顔タブの続き。眉・まぶた・目・鼻・口の変形スライダー 顔タブの続き。目・鼻・口・耳の変形スライダー

この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。体のプレフィックスです。
  • DE 00 19
    • ペアが25組(0x19)続く印。version は顔と同じ 0.0.2 です。
  • 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]となります。

髪

髪は後ろ髪・前髪・横髪・エクステの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。プレフィックスです。
  • 84
    • 「キー名と値のペアが4組続く」という印。顔の DE 00 26 と同じmapです。
      • 4組のキー名はversion / parts / kind / glossId。
      • version は 0.0.4。顔・体は 0.0.2で別々のバージョン持ってます。(なんでだろう)
  • A5 70 61 72 74 73 94
    • partsで値は4要素の配列。前から順に後ろ髪・前髪・横髪・エクステとなります。
  • 8A
    • 1個目の要素(後ろ髪)の中身で、ペアが10組続きます。
    • A2 69 64 0C
      • A2は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
髪タブの後ろ髪画面。種類はポニーテール、後ろ髪の基本の色に茶色が設定されている 髪タブの前髪画面。種類はストレート、前髪の長さは48

このような「プレフィックス」+「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」形式で、ここから服のデータが入っていることを示しています。
  • 85
    • ペアが5組続く印。キー名は version(0.0.1)/ parts / subPartsId / hideBraOpt / hideShortsOpt。
  • A5 70 61 72 74 73 99
    • partsは服の部位です。トップスから靴まで9個あります。
  • 83
    • 各部位はペアが3組だけで、id / colorInfo / emblemeId(ワッペン)です。
    • 先頭の部位(トップス)は A2 69 64 02 = id=2。ジャケットタイプを示します。
    • colorInfo 94 は4要素の配列 = 服の色スロット1〜4。各スロットは 84(ペア4組)で baseColor / pattern(柄id)/ tiling(柄の細かさ)/ patternColor を持ちます。

衣装1着分のバイナリ構成はこんな感じになります。

衣装1着ぶん(8,670 bytes) int32 clothesの長さ = 3,659 4 bytes byte[] clothes(服9部位) 各部位が id + colorInfo(色4スロット)を持つ 3,659 bytes int32 accessoryの長さ = 4,736 4 bytes byte[] accessory(アクセサリー20スロット) 各スロットが type + id + 色 + 位置/回転/拡縮 4,736 bytes bool enableMakeup = false(ここだけ生のbool) 1 byte int32 makeupの長さ = 262 4 bytes byte[] makeup(メイク) アイシャドウ / チーク / リップ / ペイントの id と色 262 bytes 4 + 3,659 + 4 + 4,736 + 1 + 4 + 262 = 8,670 bytes(ちょうど使い切る) → 次のバイトから C5 21 ED(bin16, 8,685 bytes)=2着目が同じ構造で続く
衣装1着ぶんのバイナリ構成
服タブのコピー画面。トップスのジャケットタイプからローファーまで、9部位の構成が一覧表示されている

この後はアクセサリーになります。

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。
    • アクセサリーの長さです。
  • 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]となっています。

アクセサリータブのスロット01ヘアピン。調整ウィンドウに位置-5.5/-4.3/-2.9、回転21/334/342、拡縮1.24/1.06/1が表示されている

アクセサリーの直後にある1バイトの00がenableMakeup = falseで、ここだけMessagePackを通さない生のboolです。あとは上の図のとおりで、1着目を使い切ると次のバイトから2着目が同じ構造で繰り返されます。

Parameter(キャラ設定)

キャラ設定タブ。名前は羽田絵里、愛称ははたえり、性格は純真無垢、血液型b、誕生日7月1日、部活はチアリーディング部

名前、性別、性格、血液型、誕生日、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が使われている。スライダー値からアクセサリーの位置まで、キャラメイク画面の構成がそのまま入っている。

次回はこのへんのブロックの中身をもう少し掘るか、実際にパースするスクリプトを書いてみようかと思います。