2026/02/22(日)COM3D2のキャラデータをカスタムキャストで使う方法
カスタムキャスト側でCOM3D2のプリセットを再現しようとすると、色周りの設定が数値指定できない関係で難しすぎたので、何とかしようとして見つけた方法。
一撃で同じキャラを持ってこれるのでだいぶ楽。
必要なもの
- スマホとPCを繋ぐためのUSBケーブル
- バイナリエディタ
- COM3D2
- カスタムキャスト
確認環境
| Env | Ver |
|---|---|
| Android | 16 |
| COM3D2 | 2.29.4 |
| カスタムキャスト | 1.04.01 |
やり方
- スマホをUSBケーブルでPCに繋ぐ
- スマホ側の接続形態を充電からファイル転送モードにする
- カスタムキャストを開いて適当に服・体プリセットをセーブする
内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetを開き、保存したプリセットを取り出す- COM3D2を開き、カスタムキャストで使いたいキャラのエディット画面を開き、服・体プリセットを保存する
- バイナリエディタでCOM3D2のプリセットファイルを開き、
454E44AE426082を検索し、そこより先にあるバイナリコードをすべてコピーする

- カスタムキャストのプリセットを開き、
454E44AE426082を検索し、そこより先にあるバイナリコードを、先ほどコピーしたCOM3D2のバイナリコードですべて上書きし、保存する - 保存したカスタムキャストのプリセットファイルを
内部共有ストレージ/Android/data/jp.customcast.cc2/files/Presetに戻す - カスタムキャストを開き、キャラエディットからプリセットをロードする
備考
恐らくファイルヘッダ以外ほぼ互換だと思われるが、ヘッダのリプレースが面倒なのでボディを書き換えることで達成した。
当たり前だが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();
}
}
2026/02/19(木)Value-DomainでcertbotのDNS Challengeをやるスクリプトを書き直した
投稿日:
私は2020年の末あたりからLet's EncryptのDNS-01 challengeをValue-Domainで達成するためのスクリプトであるvalue-domain-dns-cert-register (vddcr)の開発を行ってきていたのだが、メンテが辛くなってきたので、今回Shellscript(Bash)Perlでvalue-domain-dns-utilとして作り直した。その経緯と、開発中の学び、そして最後にどうやって作っていったかについて書いていく。
背景
一年半ほど前からTypeScriptに限界を感じてきたことや、当時の誤った単体テストへの偏執により、vddcrはメンテしたくない状態になってしまっていた。
そこで昨夏、つまり2025年の8月に式年遷宮、つまり全体的な書き直しを行ったのだが、個々の処理をする部品を作ったまではいいものの、それらを繋ぎ合わせるところで飽きてしまい、放置していた。vddcrはこの時点で事実上の放棄状態だった。
しかし、自宅サーバーの運用をしていく中で別の目的でValue-DomainのDNS APIと関わらなければならない必要が出てきたため、今回、vddcrの置き換えも兼ねて新しく作り直すことにしたのだ。
具体的には複数ドメインに対するDDNSをバルクでやりたい要求が生まれたが、Value-DomainにあるDDNS APIは一度に1ドメインしか処理できない上にレートリミットが60秒に1回と極めてキツく、複数ドメインに対して行うには余りにも遅延が大きかった。
例えば10ドメイン更新しようとすると10分もかかるわけで、これでは使い物にならないというので、DNS APIを直に叩いて一気に書き換える必要性が出てきたのだ。どうせ作るならvddcrが担っていたDNS challengeの機能も包含できる形にしたかったので、Value-DomainのDNS APIを便利に叩くためのツールキットとしてvalue-domain-dns-utilを作ることにした。
つまりvddcrの純粋な後継ツールというよりは、vddcrの役目もこなせるツールキットを開発し、vddcrと同等の機能を有するツールを実装サンプルとして同梱したわけだ。
Shellscriptにした理由
ものの数時間でPerlに直したので過去の話となった。移植性の関係でbashはイマイチだった。
ぶっちゃけそんなに大した理由はなくて、単に目の前にターミナルとテキストエディタがあってパッと組めたからという部分が大きい。実際ベースはパッと作れた。まぁ作って半年も放置していたのが…。ただまぁPerlの方が保守性は高いだろうし、移植性もあるだろうとは思うから、気が向いたら書き直すかもしれない。
ちなみに別言語に移植することの検討はvddcrでも行っていたが、最終的には候補になかったShellscript(Bash)での実装に落ち着いた。そもそもこの課題の存在を忘れていたので、検討すらしていなかったのである。
value-domain-dns-utilについて
今回作ったvalue-domain-dns-utilは、vddcrの単純な代替ではなく、Value-DomainのDNS APIを叩くのに便利なShellscriptの関数群だ。DNS challenge以外にも用が生まれたので、このような汎用的な形になっている。
DNS APIから現在のレコードを取得する関数や、取得したレコードから先頭一致のパターンマッチでレコードを取得する関数、レコードリストへの追加や置換をする関数、そして編集したレコードをDNS APIに送り戻す関数を備えており、DNSに関する一連の編集処理ができるように便利に作っている。
結果として、Value-DomainのDNS APIを叩く汎用機能があれば、それを流用してLet's EncryptのDNS-01 challengeもできるし、当然複数ドメインに対するDDNSをバルクでやることも可能になる。
学び
開発中に発見も多くあったので記していく。なんかこういうのまとめた記事かページ作りたいね。
echoでドルマーク始まりのシングルクォート文字列を作ると、その中の改行コードが展開される
例えば以下のようになる。これは一行で改行展開を書けて変数に入れられるので、インデントされているコードなどで物理的にへし折りたくないときなどに有用だし、文字列としてのコードが見えるのでわかりやすい部分もあると思う。
入力
#!/bin/bash
# ドルマーク始まりのシングルクォート文字列は改行が展開される
echo $'1:aaa\nbbb'
# つまりこれと同じ
echo "2:ccc
ddd"
# ダブルクォートの中の\nは展開されない
echo "3:eee\nfff"
# ドルマーク始まりのダブルクォートも展開されない
echo $"4:ggg\nhhh"
# 一行で改行展開を書けて変数に入れられるので便利
hoge=$(echo $'5:iii\njjj')
echo "$hoge"
出力
1:aaa
bbb
2:ccc
ddd
3:eee\nfff
4:ggg\nhhh
5:iii
jjj
変数の最後の一文字を削る
これは後方一致除去(${parameter%word})と呼ばれるものらしく、詳しくは調べていないが"hoge"に対してfugaを繋げ、"hogefuga"としたい場合に便利だ。
例えば以下のように書ける。
入力
#!/bin/bash
hoge='"hoge"'
fuga='fuga'
result="${hoge%?}$fuga"'"'
echo $result
出力
"hogefuga"
sedで改行を改行コードに置換(\n→\\n)
これは見つけた当時sedの記事にも書いているが、以下のようになる。
入力
cat <<'EOF' | sed ':a;N;$!ba;s/\n/\\n/g'
aaa
bbb
ccc
ddd
EOF
出力
aaa\nbbb\nccc\nddd
[]ではGlob展開できないが、[[]]では出来る
今のところこういった形式でGlobを書くことはないのだが、Sonnet 4.6にテストコードを書かせている中で発見したので書き留めておく。
入力
#!/bin/bash
hoge='aaabbbccc'
[ $hoge = *"bbb"* ]
result1=$?
[[ $hoge = *"bbb"* ]]
result2=$?
echo $result1
echo $result2
出力
1
0
あとがき
LLMとの作業分担
今回の開発でもLLMを活用したのでその辺り。
今回の開発では、8ada152~93d58ccのコミットを行った。最も古いコミットである8ada152ではテストコードを除き、私がフルスクラッチで書き、テストコードはClaude Code(Sonnet 4.6)に書かせた。
次の029a951では、私が書いたコードをClaude Code(Sonnet 4.6)に全体的に直してもらって私がレビュー・微修正するという開発形態をとった。それ以降のコミットはコメントなどの微修正なので、再び私が手動で書いている。
こうした分担により、設計の根底にある信条のようなものには私の色が濃く出ているが、実装上の問題点はClaude Codeによって大きく改善されている。特に029a951では、typoや単純な実装ミスの修正に加えて、bashの文字列処理に不慣れなことで生まれたエスケープシーケンスの山が整理された。jqの-rオプションの有無がまちまちだった部分も統一され、可読性がだいぶ向上したと思う。
とはいえ、Claude Codeの言うことを鵜呑みにはしていない。こちらから明示的に指示して直させた箇所もある。例えばテストコードのappend_recordやreplace_recordのアサートは元々Globで書かれていたのだが、より厳密にするために全文マッチに直させた。別にレコードの順序がどうなっていようとValue-Domain APIは気にしないどころか勝手にソートされるので実用上の意味はないのだが、こういうのは人としての拘りである。
昨今は過去の記事でもちょいちょい言及している通り、LLMを使った開発をそこそこしていて徐々に馴染めてきているので、その点も含めてよい経験になった。
抽象化した今回の設計と、過去の歪な設計を振り返って
抽象化して再利用性を高めた今回のツールキット方式と、単体テスト大正義とばかりに変な作りになってしまった前回の方式の振り返り。
過去のvddcrでは単体テストへの誤った偏執によりモジュールが具象的な単位で分割され、非常にメンテナンス性が悪く、再利用性もないものになっていたが、今回はちゃんと抽象化して、責務ごとに機能が切れたと思っている。このことによって再利用性が高く、使いやすいものが作れたと考えている。大した規模ではないとはいえ、部品を組み合わせて処理を組むには程よくいい感じだろう。
先ほどのことや、単純に機能を削ったこと、TypeScriptの複雑すぎる型管理や非同期処理から解放されたこともあり、コードもだいぶ見通しがすっきりした。ツールではなく、ツールキットという形で作ったことも奏功し、Value-DomainのDNS API周りのスクリプトは大分作りやすくなったと思う。将来的にバルクのDDNSスクリプトを書くのも楽になるはずだし、重い腰を上げて作れてよかった。
式年遷宮で起きる変遷について
式年遷宮をする以上、元の状態には戻れないという話があり、それが何故起きるのかみたいなところが分ったような、分からないような、そんな話。
しかし世の中には式年遷宮で全く違う姿になるものがある。今回作ったのはまさにそんな感じで、なんというか作者の気持ちがわかった気がする。
分かりづらい例えだが、FF14の外部ツールであるConcept Matrix(CMTool)がAnamnesisに変わったときは、全く別物になった気さえしたし、こういった式年遷宮によって元あったことは一応できるにはできるけど、複雑になったり、なんかかゆいところに手が届かなくなったり…みたいなのは他でもちょいちょいあると思っていて、今回vddcrをvalue-domain-dns-utilにしたのもまさにそうだと思う。
という背景があるのもあり、完全互換を目指したvd-dcr.shを同梱することで、この不便さを少しはなくそうとした。いやまぁ自分のためにやってるので、自分のためのことではあるのだが…。とはいえ、以前のと比べるとプラットフォーム別の互換性は落ちていると思うので、現時点だと動かない環境は増えている気がする。
あとがき2:結局Perlに変えた
動かそうとしてた環境の一つのシェルがashで、bashが使えないことに気が付いたのでSonnet 4.6に頼んでサッとPerlに変えてもらった。
関連記事
2026/02/18(水)布団に使えるすべり止めシートを買った
投稿日:
私は布団を床に敷いている、いわゆる万年床生活を送っているが、ここ数年布団がずれる問題に悩まされていた。
そこですべり止めを色々試していたのだが、ニトリのすべり止めシートが効果的だった。
今回購入したのはニトリの「ハサミで切れるすべり止めシート 130x185cm」だ。
布団はこんな感じに置いていて、奥の棚との間にスペースを作っているのだが、今まではこれが日に日にズレるので困っていた。
今は設置から九日立つが、全くズレていないので、非常に助かっている。
布団の構造としては、敷布団の下に厚手の防湿マットを敷いていて、その下に滑り止めマットを敷く構造になっている。
布団がずれないことによりクイックルワイパーをスムーズに挿入でき、掃除も捗るようになった。
棚と布団が接触していた時は、その隙間に埃が溜まっていて掃除に難儀していたり、右側の電源ボックスが挟み込まれてしまい、奥の引き出しを出すのが手間だったりした。これが解決したので随分楽になった。
あとがき
これまではすべり止めシールや、すべり止めテープを使っていたのだが、どうも接触する面積が少ないからか滑ったり、逆にシールが剥がれたりしてうまく機能していなかった。
今回は接触する面積がほぼ全体になり、摩擦抵抗が増えたからだろうか、うまく機能するようになったという具合だ。
2026-03-24追記
現時点でもズレていないため非常に優秀だ。
2026/02/13(金)Windows 11にnvmを入れてMSYS2のzshから使う
なんか気が付いたら動かなくなってた気がするので作業ログとして残しておく。
確認環境
| Env | Ver |
|---|---|
| Windows 11 Pro | 25H2 OS build 26200.8740 |
| MSYS2 | msys2-x86_64-20251213 |
| zsh | 5.8 (x86_64-pc-msys) |
| nvm-windows | 1.2.2 |
MSYS2 msys2-x86_64-20210725でも動作を確認しているため、MSYS2のバージョンはほぼ関係ないと思われる。
前提条件
- MSYS2とzshは既にあるものとする。
- Node.jsは入っていないものとする。
やり方
- nvm-windowsからインストーラーを落としてインストール
- 任意のシェルで
nvm onを実行 nvm install ltsなり適当なインストールコマンドを叩くnode -v && npm -vで両方のバージョンが出ればOK
トラブルシューティング
nodeやnpmのコマンドが見つからない
nvm onを実行していないと上手くパスが通らないのでこれを実行する必要がある。
あとがき
nvm onなんてしなくても動いていた気がしたが、久々にWindowsでnodeを叩こうとしたら動かなかったので、ついでに全部の環境を刷新するついでに書いた。
2026/02/13(金)Matomoで直帰ユーザーの滞在時間を取得できるようにする
投稿日:
Matomoは標準では直帰ユーザーの滞在時間が取れないが、それをできるようにする方法。
確認環境
Matomo Version 5.7.1
やり方
トラッキングコードに_paq.push(['enableHeartBeatTimer', 10]);を追加するだけ。
公式ドキュメントを読む限り、これをすることで10秒以上閲覧した場合に滞在時間が取れるようになると思われる。なお、HeartBeatTimerという名前だが、眺めてみた感じ定期的にAPIリクエストを送ったりはしていないようだった。
取り敢えず10秒以下は0と同じでいいと思ったので10にしておくことにした。ビジット継続時間単位のビジットでも11秒以上の敷居があるので、キリもいい。11秒にしたほうがよりノイズが減り、正確かもしれないが、個人的には運用上10秒が取れても害はないので、細かいことは気にしないでおく。
なお、かつては名前の通り指定時間にポーリングしていたようだ。また5秒未満を指定しても機能しないらしい。
コードの追加例
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://analytics.example.com/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
+ _paq.push(['enableHeartBeatTimer', 10]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName ('script') [0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
あとがき
公式ドキュメントの説明が薄いので、具体的にどう処理されているかはソースコードを読まないとわからないが、取り敢えず直帰ユーザーの滞在時間が見れるようになったので良かった。
一律で0sだと、内容を見ているユーザーと見てないユーザーが区別できないので、ここが分かるのは便利だ。
因みにGoogle Analyticsでも直帰は0秒扱いらしいが、そもそも最近この項目自体が削除されて、別概念に置き換わったらしい。
こういったことを踏まえると、やはり私のような個人サイト運営者にとってはMatomoは非常に便利なツールだ。







