プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

ぷち髪型再現全般に関する総合覚書

ぷち髪型再現全般に関する総合覚書

諸事情で、書き途中のメモを載せているだけです

導入方法

1. ファイル配置

https://imas.cg/ を例にとると、主に
https://imas.cg/js/hair/ 系と
https://imas.cg/js/ajax_hair_styling_list_by_idol_id 系の階層に、
今回用意したファイルをそれぞれ配置

imcg (最上位相当)
  |
  ├ js
  |  └ hair
  |     ├ 10000.js ( 千川ちひろ )
  |     ├ 10001.js
  |     ├ 10002.js
  |     ├ .....
  |     ├ 19952.js ( 本来の実装髪型末尾相当 )
  |     ├ .....
  |     └ 19999.js ( その他連番総当たりで取得されたファイル )
  |
  └ petit_cg 
      |
      └ ajax_hair_styling_list_by_idol_id
          |
          ├ index.php
          └ json
              |
              ├ petit_hair_1001.json
              ├ petit_hair_1002.json
              ├ ....
              └ petit_hair_1193.json

2. ぷちプロフのページ内スクリプト差し替え

https://imas.cg/petit_cg/coordinate_idol/[petit_id]/ の各種ページに相当する .html(.php) 内部から、
function getHairstyleList() で検索し、API URLを差し替え。

function getHairstyleList() {
    $.ajax({
        type: 'POST',
-       url: 'https://imas.cg/petit_cg/ajax_hair_styling_list_by_idol_id?l_frm=Petit_cg_coordinate_idol_1%26rnd=861092554',
        //API URL は必ずクエリ系のゴミを除去して、正しく / で閉じる事
+       url: 'https://imas.cg/petit_cg/ajax_hair_styling_list_by_idol_id/',
        dataType: 'json',
        data: {
            "idol_id": mc.character[default_chara].idol_id
        }
  • 【注】アクセスするAPIのURLはキレイに書き直しておく事。

    余計なクエリなどを付けたままだと QueryStringParameters
    l_frm : Petit_cg_coordinate_idol_1&rnd=861092554 みたいな形式となってしまい、
    POST のはずなのに GET 扱いになったりするので注意。

API 動作確認用スクリプト

~/petit_cg/ajax_hair_styling_list_by_idol_id/
index.php と、petit_hair_1XXX.json が統括されている /json/ フォルダを配置したら、恐らくAPIとして機能する筈?

以下をコンソールからたたいて確認。
それっぽい JSON が返れば成功。

ぷちコーディネートページの髪型取得処理を抜き出し & 簡略化したもの。

(()=>{
    $.ajax({
        type: 'POST',
        url: 'https://imas.cg/petit_cg/ajax_hair_styling_list_by_idol_id/',
        dataType: 'json',
        data: {
            "idol_id": 1023 // 仮 @ 高森藍子
        }
    }).done(function(data) {
        console.info(data);
    });
})();

上下どちらも同性質の処理です

(()=>{
    const _url = 'https://imas.cg/petit_cg/ajax_hair_styling_list_by_idol_id/';
    const _req = new XMLHttpRequest();
    _req.open("POST", _url , true);
    _req.responseType = "json";
    _req.onload = function(oEvent) {
        const _data = _req.response;
        console.log(_data);
    };
    const _formData = new FormData();
    _formData.append( "idol_id" , "1023" ); // 仮 @ 高森藍子
    _req.send( _formData ); 
})();

▼ 以下は覚え書きなどです。


ぷち髪型再現に必要と思われる仕組み/情報など

  1. /ajax_hair_styling_list_by_idol_id/ API および そこから得られる情報の再現
  2. /js/petit_cg/hair/XXXXX.js ファイル群

多分この2つだけでOKぽい

API仕様メモ

ぷちプロフ(着せ替えページ)上で「ヘアスタイリング」タブ選択時に呼ばれる模様
「着せ替え」というよりは 「髪型変更の選択肢を表示する機能

髪型選択時、以下が FormData で POST されている。
value は4ケタのぷちデレラID ( 1001 島村卯月 ~ 1193 久川颯 )

{
  idol_id : 1001
}

API 応答内容

POSTされた ぷちデレラIDに対応する髪型系?JSONを返す。
JSON内にセリフなども含まれている模様
公式のAPI仕様だと、そのアカウントで解放済みのぷち髪型のみ返る
(=そのぷちで実装されている全髪型が得られるとは限らない)ので、取得検証には注意が必要。

コード的には index.php を通じて /json/petit_hair_1XXX.json の中身を出力してるだけ。

ちな、応答オブジェクト内にある idol_message (セリフ) は、
本来であれば2種類のぷちセリフからどちらかランダムで出力される模様。
※ 今回の疑似APIは static な json を出力しているだけなのでセリフ1種固定。

定義js

/js/petit_cg/hair/[ID].js に存在。
5ケタの数値から成っている。
先のAPIから得たJSON内部に含まれている髪型IDが、ここの .js ファイル名と連動しているため、
有効な髪型IDさえ判明すれば、当該する髪型定義 .js ファイルも確保可能。

    • 10001 卯月の髪型 なら
      /js/petit_cg/hair/10001.js
    • 11001 卯月+の髪型 なら
      /js/petit_cg/hair/11001.js
    • 19001 卯月+の髪型(ハーフアップ左) なら
      /js/petit_cg/hair/19001.js

衛藤美紗希 19952 美紗希+の髪型(アクセ無し) が恐らく最大値?

10000 は千川ちひろ相当 ( リフレッシュルームとかで使われる筈 )

jsのファイル数について

本来であれば全ぷちデレラ 10001 卯月 ~ 10193 久川颯 の実装開放髪型数が 398(のハズ)
10000 が千川ちひろ相当なので、さらに +1 = 399ファイル

更に、サーバーに推測総当たりで 20500 番台までアクセスし、有効DLされた物をさらに追加。
ざっと垣間見た限り、内容(画像としてのビジュアル)が被っている髪型っぽい?
とはいえ精査はしておらず「とりあえずアクセス出来たからDL確保したものをそのまま持たせている」感じです。

髪型定義 .js の中身・base64 とか

base64 png 形式で描かれた画像が複数 key-value ペアで格納されている模様。

https://base64.guru/converter/decode/image/png
うまくコピペすれば、この辺のテキトーなデコードサイトで髪型のパーツごとに覗ける。
が、もう少しサクッと確認できるスクリプトを以下に組んだ。

通信 + DevTool経由で髪型を可視化させてみる。

ちょっとした簡易お遊び確認用。
_accessDefList で確認ID範囲を設定し、
_dlInfo.accessUrl のURLで、staticな 髪型定義 .js のURLを指定。
Console上で実行すると、その定義内で使われている画像がログで可視化される


(async()=>{
    class DownloadInfoItem{
        /**@type {string} アクセス先URL*/
        accessUrl;
        /**@type {string|number} 対象ID */
        targetId;
    };
    const _accessDefList = [
        // ★ オプション1 @ [0]~[1]までの範囲を対象
        [ 10000 , 10010 ],
    ];

    /** @type {Array.<DownloadInfoItem>} */
    const _dlTargetInfoList = [];

    // DL範囲の配列を抱えた親配列
    _accessDefList.forEach(_rangeArr=>{
        const _minFromId = _rangeArr[0];
        const _maxToId = _rangeArr[1];
        const _rangeCount = _maxToId - _minFromId;
        if(0 > _rangeCount ){ throw "範囲逆じゃない?"; }
        console.info(`${_minFromId} ~ ${_maxToId} まで アクセス 個数 → ` , (_rangeCount+1));
        for(let _targetId=_minFromId; _targetId<=_maxToId; _targetId++){
            const _dlInfo = new DownloadInfoItem();
            // ★ オプション2 @ アクセスしたい js ファイルのURLに書き換え
            //_dlInfo.accessUrl = `https://imas.cg/js/petit_cg/hair/${_targetId}.js`;
            _dlInfo.accessUrl = `https://mobamas.net/idolmaster/js/petit_cg/hair/${_targetId}.js`;
            _dlInfo.targetId = _targetId;
            _dlTargetInfoList.push( _dlInfo );
        }
    });

    function b64pngConsole(_obj){
        for(let _key in _obj){
            const _val = _obj[_key];
            if(_val.indexOf("data:image/png;base64,")==0){
                console.log('%c ', `background-image: url(${_val});
                    background-size: contain;background-repeat: no-repeat;
                    border: solid 1px gray;font-size: 0;padding: 50px;`
                );
            }
        }
    }

    _dlTargetInfoList.forEach(_dlInfo=>{
        const _hairId = _dlInfo.targetId;
        const image_script = document.createElement('script');
        image_script.onload = function() {
            const _targetvariable = window[`hair_${_hairId}`];
            console.info(_hairId);
            b64pngConsole(_targetvariable);
        }
        image_script.src = _dlInfo.accessUrl;
        document.getElementsByTagName('head')[0].appendChild(image_script);

    });
})();

以上 ✨