投稿日:

なんかこんな感じのやつの作り方。

内容

左側にあるカラーパレットを出すボタンはinput type="color"で実現できるので、単純なカラー入力だけならこれでも構わない。現時点ではAndroid版Firefoxを除き、主要ブラウザは

ただ、コードも欲しいことが往々だと思うので、その場合は横にinput type="text"を生やして、次に示すサンプルコードのように双方の入力値をバインディングしてやればよい。

サンプルコード

動きはデモページで確認できる。

<input id="palette" type="color">
<input id="code" type="text">
<script>
  const getInput = () => {
    return {
      palette: document.getElementById('palette'),
      code: document.getElementById('code')
    };
  };

  const registerHandlers = (paletteEl, codeEl) => {
    paletteEl.addEventListener('input', (e) => {
      codeEl.value = e.target.value;
    });
    codeEl.addEventListener('input', (e) => {
      paletteEl.value = e.target.value;
    });
  };

  window.addEventListener('DOMContentLoaded', () => {
    const input = getInput();
    registerHandlers(input.palette, input.code);
  });
</script>

Safariでも表示できるが見た目がだいぶ異なる

PC Safari 18.2とiOS 26.3のSafariで確認した見た目。

一行目が初期表示、二行目がカラーパレット展開表示。iOS Safariの方は素の状態だとUIが小さすぎて見えないので、かなり拡大して表示している。

PC Safari iOS Safari
PC Safari初期表示
iOS Safari初期表示
PC Safariカラーパレット展開表示
iOS Safariカラーパレット展開表示
投稿日:

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