COM3D2のキャラデータをカスタムキャストで使う方法

投稿日:

カスタムキャスト側でCOM3D2のプリセットを再現しようとすると、色周りの設定が数値指定できない関係で難しすぎたので、何とかしようとして見つけた方法。

一撃で同じキャラを持ってこれるのでだいぶ楽。

必要なもの

  • スマホとPCを繋ぐためのUSBケーブル
  • バイナリエディタ
  • COM3D2
  • カスタムキャスト

確認環境

Env Ver
Android 16
COM3D2 2.29.4
カスタムキャスト 1.04.01

やり方

  1. スマホをUSBケーブルでPCに繋ぐ
  2. スマホ側の接続形態を充電からファイル転送モードにする
  3. カスタムキャストを開いて適当に服・体プリセットをセーブする
  4. 内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetを開き、保存したプリセットを取り出す
  5. COM3D2を開き、カスタムキャストで使いたいキャラのエディット画面を開き、服・体プリセットを保存する
  6. バイナリエディタでCOM3D2のプリセットファイルを開き、454E44AE426082を検索し、そこより先にあるバイナリコードをすべてコピーする
  7. カスタムキャストのプリセットを開き、454E44AE426082を検索し、そこより先にあるバイナリコードを、先ほどコピーしたCOM3D2のバイナリコードですべて上書きし、保存する
  8. 保存したカスタムキャストのプリセットファイルを内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetに戻す
  9. カスタムキャストを開き、キャラエディットからプリセットをロードする

備考

恐らくファイルヘッダ以外ほぼ互換だと思われるが、ヘッダのリプレースが面倒なのでボディを書き換えることで達成した。

当たり前だがMODとかは反映されない。

おまけ

データを個別に取得して弄れるようにしようとツールを書こうとしていたが、パラメーターが多いのと、フォーマットが異なるものがあり、解析に飽きたので調べた残骸として残しておく。

using System.Diagnostics;
using System.Text;

class Program {
    static void Main(string[] args) {
        string filePath = @"C:\path\to\pre_Dummymaid_20260222134142.preset";

        if (!File.Exists(filePath)) {
            Debug.WriteLine($"ファイルが見つかりません: {filePath}");
            return;
        }

        byte[] fileData = File.ReadAllBytes(filePath);
        Debug.WriteLine($"ファイル読み込み完了: {fileData.Length} bytes");

        var obj = new ParamFinder(fileData);

        var MuneL = obj.FindBodyValue("MuneL");
        var MuneS = obj.FindBodyValue("MuneS");
        var MuneTare = obj.FindBodyValue("MuneTare");
        var RegFat = obj.FindBodyValue("RegFat");
        var Hara = obj.FindBodyValue("Hara");
        var RegMeet = obj.FindBodyValue("RegMeet");
        var KubiScl = obj.FindBodyValue("KubiScl");
        var UdeScl = obj.FindBodyValue("UdeScl");
        var EyeSclX = obj.FindBodyValue("EyeSclX");
        var EyeSclY = obj.FindBodyValue("EyeSclY");
        var EyePosX = obj.FindBodyValue("EyePosX");
        var EyePosY = obj.FindBodyValue("EyePosY");
        var EyeClose = obj.FindBodyValue("EyeClose");
        var EyeBallPosX = obj.FindBodyValue("EyeBallPosX");
        var EyeBallPosY = obj.FindBodyValue("EyeBallPosY");
        var EyeBallSclX = obj.FindBodyValue("EyeBallSclX");
        var EyeBallSclY = obj.FindBodyValue("EyeBallSclY");
        var FaceShape = obj.FindBodyValue("FaceShape");
        var MayuY = obj.FindBodyValue("MayuY");
        var HeadX = obj.FindBodyValue("HeadX");
        var HeadY = obj.FindBodyValue("HeadY");
        var DouPer = obj.FindBodyValue("DouPer");
        var Sintyou = obj.FindBodyValue("sintyou");
        var Koshi = obj.FindBodyValue("koshi");
        var Kata = obj.FindBodyValue("kata");
        var West = obj.FindBodyValue("west");
        var MuneUpDown = obj.FindBodyValue("MuneUpDown");
        var MuneYori = obj.FindBodyValue("MuneYori");
        Debug.WriteLine($"MuneL: {MuneL}, MuneS: {MuneS}, MuneTare: {MuneTare}, RegFat: {RegFat}, Hara: {Hara}, RegMeet: {RegMeet}, KubiScl: {KubiScl}, UdeScl: {UdeScl}, EyeSclX: {EyeSclX}, EyeSclY: {EyeSclY}, EyePosX: {EyePosX}, EyePosY: {EyePosY}, EyeClose: {EyeClose}, EyeBallPosX: {EyeBallPosX}, EyeBallPosY: {EyeBallPosY}, EyeBallSclX: {EyeBallSclX}, EyeBallSclY: {EyeBallSclY}, FaceShape: {FaceShape}, MayuY: {MayuY}, HeadX: {HeadX}, HeadY: {HeadY}, DouPer: {DouPer}, Sintyou: {Sintyou}, Koshi: {Koshi}, Kata: {Kata}, West: {West}, MuneUpDown: {MuneUpDown}, MuneYori: {MuneYori}");
    }
}

class ParamFinder {
    private Byte[] FileData;

    static byte[] CreateBinaryStringBytes(string text) {
        using var ms = new MemoryStream();
        using var writer = new BinaryWriter(ms, Encoding.UTF8);
        writer.Write(text);
        writer.Flush();
        return ms.ToArray();
    }

    /// <summary>
    /// バイト配列内から指定パターンの最初の出現位置を返す (見つからなければ -1)
    /// </summary>
    static int FindPattern(byte[] data, byte[] pattern) {
        int limit = data.Length - pattern.Length;
        for (int i = 0; i <= limit; i++) {
            bool match = true;
            for (int j = 0; j < pattern.Length; j++) {
                if (data[i + j] != pattern[j]) {
                    match = false;
                    break;
                }
            }
            if (match)
                return i;
        }
        return -1;
    }

    public ParamFinder(Byte[] fileData) {
        this.FileData = fileData;
    }

    public int FindBodyValue(string subject) {
        byte[] signature = CreateBinaryStringBytes(subject);

        int sigOffset = FindPattern(this.FileData, signature);
        if (sigOffset < 0) {
            throw new Exception($"エラー: データセクションのシグネチャ '${subject}' が見つかりませんでした。");
        }

        using var ms = new MemoryStream(this.FileData, sigOffset, this.FileData.Length - sigOffset);
        using var reader = new BinaryReader(ms, Encoding.UTF8);
        reader.ReadString();
        reader.ReadString();
        reader.ReadInt32();
        reader.ReadInt32();
        reader.ReadString();
        reader.ReadInt32();
        reader.ReadInt32();
        return reader.ReadInt32();
    }

}