投稿日:

カスタムキャスト側で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();
    }

}
投稿日:

既存CGIを一切触らずに、前段にアクセス制御やアクセスロガーをつけたいとかの用途でラッパーCGIを使うと上手くいくので、その方法を書く。

なお前段が動くことしか試していないが、備考に後段で処理をさせる方法についても軽く触れている。

動作機序

CGIはコマンドライン引数、環境変数、標準入力を受け取り、何かを処理した結果を標準出力するプログラムである。

つまりラッパーCGIはコマンドライン引数、環境変数、標準入力を受け取り、それをラッピングするCGIにそのまま受け渡し、このCGIの標準出力をリダイレクトできればよい。

やり方

以下のようなコードを書き、exec()前に前段の処理を書けばよい。

#!/usr/bin/perl
use strict;
use warnings;

#
# ここに実行前に挟みたい処理
#

my $original_cgi = './hoge.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";

今回実際に作ったサンプル

adiaryにはアクセス制限をする機能がなく、本体を弄るのが嫌だったので前段に処理を入れることで実現した。

#!/usr/bin/perl
use strict;
use warnings;

# 以下のコマンドでIP::Geolocation::MMDBをインストールしていることが前提
# cpanm -l extlib IP::Geolocation::MMDB
use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;
# https://download.db-ip.com/free/dbip-city-lite-YYYY-MM.mmdb.gz
# ex. https://download.db-ip.com/free/dbip-city-lite-2026-01.mmdb.gz
my $db = IP::Geolocation::MMDB->new(file => './DBIP-City.mmdb');
my $country_code = $db->getcc($ENV{REMOTE_ADDR});

# 日台韓は許可する方針(怪しい挙動を見たことがないため)
my @allow_country_codes = ('JP', 'TW', 'KR');

my $user_agent = $ENV{HTTP_USER_AGENT};

# 許可する国コードかチェック
my $is_allowed_country = grep { $_ eq $country_code } @allow_country_codes;

# 許可するBOTのUAパターン
my @allowed_bot_patterns = (
    qr/bot/i,
    qr/curl/i,
    qr/wget/i,
    qr/google/i,
    qr/bing/i,
    qr/mastodon/i,
    qr/misskey/i,
    qr/pleroma/i,
    qr/akkoma/i,
    qr/lemmy/i,
    qr/activitypub/i,
    qr/hatena/i,
    qr/github/i,
    qr/tumblr/i,
    qr/meta/i
);

# 許可するBOTかチェック
my $is_allowed_bot = 0;
for my $pattern (@allowed_bot_patterns) {
    if ($user_agent =~ $pattern) {
        $is_allowed_bot = 1;
        last;
    }
}

# 許可国でもなく、許可BOTでもなければエラーにする
if (!$is_allowed_country && !$is_allowed_bot) {
    # 未知のSNS BOTを将来的に許可するために、BOTくさいUAのログを集めておく
    if ($user_agent !~ /Windows|Mac OS|Linux|Android|iOS|iPhone|iPad/i) {
        # OGP取得BOTに間違いなく含まれない文字列が入ってるものはログに入れない
        my $deny_ua_log_file = './deny_ua.log';
        if (open my $fh, '>>', $deny_ua_log_file) {
           my $time = localtime();
           my $remote = $ENV{REMOTE_ADDR} // 'unknown';
           my $uri = $ENV{REQUEST_URI} // 'unknown';
           print $fh "[$time]\t\"$user_agent\"\t$country_code\t$remote\t$uri\n";
           close $fh;
        }
    }

    print "Status: 403 Forbidden\n";
    print "Content-Type: text/plain; charset=UTF-8\n\n";
    print "Access denied.\n";
    exit;
}

# adiary呼び出し
my $original_cgi = './adiary.cgi';
exec($original_cgi) or die "Cannot exec $original_cgi: $!";

備考

perldocを読んだ感じ、互換性に問題が出る可能性も少なからずあるようだ。

perldocのexec関数の説明を見る感じ、ENDブロックや、オブジェクトのDESTROYメソッドを起動しないとあるので、実装方法次第では正しく動かない可能性もあるのかもしれない。

また「戻って欲しい場合には、execではなく system関数を使ってください」とあるため、もし後処理をしたい場合はexec関数でなくsystem関数を使うとよいと思う。

あとがき

レンタルサーバーではWAFが自由に使えないため、なんちゃってWAFの様なものを作りたいとか、レンタルサーバーを新規に始めたく、CGIにバナー広告を差し込みたいといったケースがある場合に、今回のような手法は便利だろう。

今時、往年のレンタルサーバーを新規に始め、それもバナー広告を出したいと考える人物がいるかどうかは謎だが、共通的に何かを差し込みたいなど、何かしら活用方法はあるかもしれない。

ググって出てきた記事が軒並み古くて役に立たなかったので、令和八年最新版として書いておく。

やり方

  1. CPANMのインストール
    curl -L https://cpanmin.us | perl - App::cpanminus
    
  2. CPANMのパスを.cshrcに書く
    私はZSHを使っているため.zshrcに書いているが、デフォルト環境はcshのはずなので.cshrcに書けば成り立つと思う。
    echo PATH=${HOME}/perl5/bin:${PATH}
    
  3. local::libをインストールする
    local::libはroot以外にあるCPANモジュールを使うためのものらしい。これを自分のホームディレクトリ配下に入れるようにコマンドを流す。
    cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
    

使い方

以下のようなコマンドを流すとパスを指定してCPANモジュールを取得・展開できる。

# 書式
cpanm -l <パス> <モジュール名>

例えば以下を流すとpwd配下にextlibディレクトリが生成され、IP::Geolocation::MMDBがダウンロードされて展開される。勿論、依存関係も勝手に解決してくれる。

cpanm -l extlib IP::Geolocation::MMDB

モジュールを利用するときはライブラリの配置されているルートを指定し、次にモジュール名を指定するとうまくいくようだ。

use lib './extlib/lib/perl5';
use IP::Geolocation::MMDB;

あとがき

https://cpanmin.us/を見に行くと以下の記述があり、辿ってみると日本の人が作っていてちょっと驚いた。

# This is a pre-compiled source code for the cpanm (cpanminus) program.
# For more details about how to install cpanm, go to the following URL:
#
# https://github.com/miyagawa/cpanminus
投稿日:

日本国内からのアクセスを識別するときに、IPv4の頃はリモートホスト名が/\.jp$/であるかや、nifty.comみたいな例外を個別で見てやる程度で何とかなっていたが、IPv6は逆引きができないので、それをどうにか解決する方法。

確認環境

Env Ver
PHP 8.3.8
geoip2/geoip2 3.2.0

やり方

  1. ComposerでGeoIp2を入れる
    php composer.phar require geoip2/geoip2
    
  2. dbipからIP to City Lite databaseのMMDBを落としてくる。
    https://download.db-ip.com/free/dbip-city-lite-2025-11.mmdb.gzのURL形式なのでYYYYとMMのとこをいじっても落とせる。
  3. こんな感じにコードを書く

    <?php
    require 'vendor/autoload.php';
    use GeoIp2\Database\Reader;
    
    // 落としてきたMMDBを指定
    $cityDbReader = new Reader('dbip-city-lite-2025-11.mmdb');
    
    // IPを渡すと結果が取れる
    $record = $cityDbReader->city('2403:6200:8860:e103:a4b2:ff66:9e68:6436');
    
    print($record->country->isoCode . "\n"); // 'US'
    print($record->country->name . "\n"); // 'United States'
    print($record->country->names['zh-CN'] . "\n"); // '美国'
    
    print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
    print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
    
    print($record->city->name . "\n"); // 'Minneapolis'
    
    print($record->postal->code . "\n"); // '55455'
    
    print($record->location->latitude . "\n"); // 44.9733
    print($record->location->longitude . "\n"); // -93.2323
    
    print($record->traits->network . "\n"); // '128.101.101.101/32'
    

参考

あとがき

国情報を利用してアクセスを弾く必要があったので雑に調べて導入したが、dbipの無料DBは正確性が低いので、個人の小規模利用向けだと思う。

エンタープライズ用途などで制度が必要な場合は有料DBの契約を検討したほうがいいだろう。

しかしGeoIp2はPHPのマニュアルにあるからPHPに組み込みで存在してるのかと思ったら、まさかのComposerで驚いた。PHPも変わったなぁ…とかしみじみ。

でも昔からPearはあったような気もするし、今更か…?

更新日:
投稿日:

こんな感じにフォーカスが当たるとOS標準の枠が出て、矢印キーの左右で選択状態を切り替えられる<input type="radio" />要素の作り方。

大まかには<label><input type="radio" /></label>の構造にして、UI上はラベルを疑似要素的に表示し、ラジオボタン本体はUI上見えなくするが、display: none;にせず、画面に残すことによってタブ遷移できるフォーカス可能要素として設計する内容。

サンプルコード

デモページ

CSS

/* ラジオボタンを等間隔に整列し、左右に余計な空白を持たせない */
.radio-container {
    display: flex;
    column-gap: 0.25rem;
}

.hidden-radio {
    /* チェックを消す */
    appearance: none;

    /* 非推奨要素を使っているが、iOS Safari対策 */    
    position: absolute;
    clip: rect(0, 0, 0, 0);
    pointer-events: none;
    
    /* デフォルトマージンがチェックボックスの位置にあるので消す */
    margin: 0;
}

.radio-label {
    /* 文字列要素を綺麗に中央寄せする */
    display: flex;
    justify-content: center;
    align-items: center;

    /* 枠装飾、基本的にフォーカス枠が角丸であり、違和感がないようにborderも角丸にしておく */
    padding: 5px;
    border: 1px solid #60bce9;
    border-radius: 4px;

    /* 見かけ上ボタンなのでカーソルをボタン用にする */
    cursor: pointer;
}

/* ラジオチェック時にラベルの背景色を変化させる */
.radio-label:has(.hidden-radio:checked) {
    background-color: #60bce9;
}

/* ラジオフォーカス時にラベルのフォーカス枠を出す */
.radio-label:has(.hidden-radio:checked):focus-within {
    outline: auto;
}

HTML

<form>
    <div class="radio-container">
        <label class="radio-label">
            <input name="r2" type="radio" class="hidden-radio" value="1" checked />
            <span>道明寺</span>
        </label>
        <label class="radio-label">
            <input name="r2" type="radio" class="hidden-radio" value="2" />
            <span>長命寺</span>
        </label>
    </div>
</form>