カスタムキャスト側で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();
}
}
投稿日:
インストール
https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script
wget https://dot.net/v1/dotnet-install.sh
chmod 755 dotnet-install.sh
./dotnet-install.sh
echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.zshrc
echo 'export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools' >> ~/.zshrc
Hello world
https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet
dotnet new console -o myapp
dotnet build myapp/myapp.csproj
./myapp/bin/Debug/net6.0/myapp
投稿日:
Windows向けのツールを作ろうというので、昔よく使っていたC#.NETで開発をすることにしたのですが結構苦労があったのでその話を書いていこうと思います。Unit testingを書くのは一旦断念しましたが、暇があれば挑戦したいとは思ってます。
事のいきさつ
WindowsのVSCodeでRemote Development拡張を使うことが多いのですが、リモートエクスプローラーにアイテムが増えてくると移動したいフォルダを探すのが大変になってくるのと、ここに出てないフォルダを開くのが結構手間という問題があり、これを解決できないかということを考えていました。
そこで考えついたのがWSLやSSHのネットワークパス上でExplorerの右クリックメニューを開いたらVSCodeがリモートモードで起動できると良いのでは?という所でした。Windows向けでexeからバイナリ起動するならC#で書くのが楽だろうと考え、C#での開発に着手しました。
疎結合な実装でUnit testingもできたらいいなーと漠然と考えながら開発していたのですが、最後にC#を書いたのは3年ほど前で、C#でモダンな実装をした経験もなかったので結構苦労しました。よく考えたらTypeScriptかJavaScriptくらいでしかまともにやったことがないし、UT自体他言語でもGolangかPHPでしか書いたことがなかったので、C#でこれをやるのは自分の実装技術に対する一種の挑戦みたいなところがありました。結論としてはあっけなく破れたわけですが…。
ツールの要件
- Explorerの右クリックメニューから起動する
- 開いたパスに応じてWindowsローカル、SSH、WSLを判定し、適切なモードでVSCodeを起動する
- パス解決には設定ファイルを用いる
- Explorerから渡されるパスはリモート環境のパスと差異があるので、そこを解決するためのものです
ツールの設計
やることは設定ファイルを読み込み、コマンドライン引数から値を取得し、それらをいい感じに変換してVSCodeに渡すだけです
大まかなフロー
実行形式
- コマンドライン引数
this.exe <dir path>
-
設定ファイル形式
```json5
{
"CodePath": "VSCodeのexeパス",
// リモートホスト定義。辞書形式
"Remote": {
"リモートホスト名": {
"ExplorerPrefix": "explorerのパスから除外する文字列",
"AppendPrefix": "explorerのパスに追加する文字列"
},
}
}
```
ツールの実装
まずはTDD的にやろうとしてInterfaceを作成し、Classで実装し、ロジック自体は好調に組み上がっていき、テストもすべて通るようになりました。但し実際にexeを蹴ると動かず、テストは通るが動かないゴミが出来上がっていました。(途中で動作確認をしなかったのか?と思うかもしれませんが、実際は完全に動くものを作ってからテスト可能な形に書き換えるという流れで作っていたのでこうなっています)
動かなかった理由は設定ファイルにJSONを採用していて、デシリアライザにSystem.Text.Json.JsonSerializerを使っていたためです。このデシリアライザは標準ではInterfaceに対してJSONをデシリアライズできません(考えてみれば当然ですが)。しかし、テストをするためにはモックをDIしたいので、Interfaceが必要です。
以下は実際に設定を読み込むために実装したClassですが、メンバが全部Interfaceになっているので、標準の状態ではデシリアライズに失敗します。ならばデシリアライザを自作すればいいという話が出てくるのですが、高々設定ファイルを読み込む処理にそこまで情熱を込めるか…?と言うことになり、諦めました。
public class ConfigBase : IConfigBase {
private IFileInfo? _CodePath;
private IDictionary<string, IConfigRemote>? _Remote;
public IFileInfo CodePath {
get {
if (this._CodePath == null) {
throw new Exception("Config Error: Missing CodePath.");
} else if (this._CodePath == null) {
throw new Exception("Config Error: CodePath is empty. Set the code.exe path in this field.");
} else if (!this._CodePath.Exists) {
throw new Exception("Config Error: CodePath is Not exists.");
} else {
return this._CodePath;
}
}
set { this._CodePath = value; }
}
public IDictionary<string, IConfigRemote> Remote {
get {
if (this._Remote == null) {
throw new Exception("Config Error: Missing Remote.");
} else if (this._Remote.Count == 0) {
throw new Exception("Config Error: Remote is empty. Set the remote infomation in this field.");
} else {
return this._Remote;
}
}
set { this._Remote = value; }
}
}
そもそもこのツールは「設定ファイルを読み込み、コマンドライン引数から値を取得し、それらをいい感じに変換してVSCodeに渡すだけ」のツールです。たったそれだけのツールに入れる仕組みにしては大げさすぎると感じました。
テストをするためにInterfaceを作ったり、本実装とモック用のClassを作る程度まではまだ容認できるのですが、カスタムデシリアライザを作ると、今度はそれのテストも必要になってきます。どう考えてもしんどい。
因みにこのコード、仮にInterfaceをやめても文字列をFileInfoに組み替えるためのカスタムデシリアライザの実装が必要で結構頭が痛くなります…。多分そこはFile.existsをラップしたクラスをDIしてやるのが無難な気がしますね。
この辺はTypeScriptだとimportの中身をJestで書き換えたら終わりなのであんま考えなくていいのは楽ですが、C#だと厳しいなと感じました。
C#でテストを書いていて思ったこと
クラスベースで実装していくとメソッド単位でのテストがしづらく、テストがコケても原因が把握しづらいというのを一つ課題感として覚えました。TSでなんちゃって関数型開発をしていれば関数のUTを書けば関数の挙動を把握できますが、Classではそうも行きません。
仮に1つのpublic methodが5つのprivate methodを呼び出していて、private methodの裏ではprivate propertyが複雑な依存を持っていたとしたらどうでしょうか?
正直デバッグをかけないとどこで何がコケたか特定できないと思います。そんな実装にするのが悪いといえばそうですが、でもクラスってそういうものじゃないですっけ…?お互いに関連性がなくていいならもうそれ関数で良くないですか?って思いました。
勿論、全部静的メソッドにしてClassそのものは単なるエンティティにするのも選択肢の一つだとは思います。しかしC#でそこまでやるか…?という疑念が個人的にあるのと、処理を繋げたテストをUTとして書く方法がなくなると思います。例えばインスタンスメソッドならモックをDIすることでメソッドが呼ばれた事や、戻り値に対する分岐を確認できますが、静的メソッドでこれをやるのは難しいと思います。
Classの単体テストは関数と状態が密結合したテストになってしまうので、いまいち微妙だなと思ったのが今回思ったことでした。
投稿日:
TypeScriptやJavaScriptだとコマンド一発で見れるコードカバレッジだが、C#.NETの開発ではちょっと手こずったので導入方法を記録しておく。
確認環境
| Env | Ver |
|---|---|
| Visual Studio 2022 Community | Version 17.5.1 |
| .NET Framework | 6.0 |
導入手順
- C#.NETで開発用のプロジェクトを作成
- 作成したプロジェクトのソリューションにxUnitテストプロジェクトを追加
- 何かしらの実装と、それに対するテスト実装を作成
- Fine Code CoverageをVisual Studioにインストール
- ソリューションエクスプローラからxUnitテストプロジェクトを右クリックしてテストの実行
- 下部トレイにあるFine Code Coverageタブを選択
- コードカバレッジが表示される
疑問
Visual Studio 2022にもなって標準でコードカバレッジも取れないの?
今どきそんな事あるか?思って調べてみたところ、どうやらEnterpriseであればコードカバレッジを取る機能がついている模様。
まぁ無料版だから仕方ないねということで諦めるしかないでしょう。Professional版にもないけど…w。VisualStudioの便利機能は以前から有料機能に組み込まれる傾向があるので仕方がない気もしますが、取り敢えず今回は有志が便利なツールを作っていてくれて助かりました。
因みにコードカバレッジ自体は無料版でも取れます。ただこれはXMLのカバレッジレポートを吐くだけなので、可視化するには別途ここからHTMLを生成する必要があり面倒なので、なんかもっと楽な方法はないかなと思って見つけたのが今回のFine Code Coverageでした。
しかしFine Code Coverage便利なのにそこまで利用されているように見えないのは、やはりテストに関心がある人が少ないのか、公式にあるカバレッジHTMLを吐く方法で納得しているのか、その辺りが気になりました。自前でFine Code Coverageの様な物を作っている人もいるでしょうが、それは少数派だと思いますし。
この方法で取れるカバレッジは正しいものか?
GitHubのREADMEを読む限り、組み込みのカバレッジレポーターのAPIを叩いて取ってきたものを表示しているだけに見えるので、恐らく正しいデータが出ているのではないかと思います。
少なくとも単純なコードのUnit testingを書いた感じではテストケースを増減することでカバレッジも連動して増減していたのと、組み込みのレポーターがそのくらいはやってくれている筈で、これはその内容を表示しているだけなので恐らく大丈夫なのではないかと考えています。







