新しもの好きプログラマの耳より情報ブログ

仕事でもあるプログラミングについて役に立ちそうな情報を発信していこうというブログです。役に立たなそうな情報はfacebookで。

VRもくもく会を今週もやりました&その様子の写真

VRもくもく会を今週も開催しました。これは、「VRでのイベントというのも今後は選択肢に入ってくるのではないか、とにかくやってみて経験値を溜めてみよう」ということでやっています。2~3週間おきの定期開催ということで、もう1年以上続けています。

今日は参加する人の都合が合わず、一人でもくもく会となりました。まあこういう時もありますが、続けていくのが大事かなと思ってます。 今日の成果は・・・Microsoft Entra IDを使う機会が来そうだったので、久々にネイティブアプリからの認証部分を書いてみました。だいぶ昔に素のOAuthで書いたことはありましたが、MSALを使うととても楽ですねえ。

会場の様子を貼っておきます。

保守性(変更の容易さ)を上げる、現実的にすぐ使えそうなテクニック。あるいはオブジェクト指向のエッセンス その1

概要

保守性を上げるためのコーディングの理論は、オブジェクト指向をはじめとして、いくつもあります。しかし、現場では理想とのギャップがありすぎて、「理論としては学んだが、現実に使うものではない」というような扱いを受けがちだと感じます。

0か100かではなく、まず1から少しずつやっていこうとした場合、何からやれば良いか。現場ですぐ取り入れやすく、かつ本質的に重要だと思う部分を切り出して紹介していきます。この記事は第一弾という感じで、反響次第で続けていこうと思います。

最初に結論まとめ

  • ポイント1:仕様に出てくる要素に、日本語と英語で名前を付けましょう。コード上の関数・変数名を、その名前で統一しましょう。

オブジェクト指向を使おうが使うまいが、これは保守性を大きく左右する重要ポイントです。理論的なキーワードはたぶん、「ドメイン分析」「モデリング」といったあたりと近いでしょう。

なぜこの記事を書いたか

オブジェクト指向UML、クラス図、シーケンス図、ドメイン駆動設計。たいていのプログラマは、こうした用語を聞いたことは有ると思います。しかし同時に、学問として学んだだけで、プログラムの現場では使っていないという人が多いのではないでしょうか。

その理由を聞いてみると、「時間の無駄だと思われている」あるいは逆に「一から作る案件で完璧にやらないと意味が無い」といった話が多いように思います。

私は、どちらも極端すぎる勘違いだと思います。部分的に取り組むだけでも、明らかに保守性が上がる効果が得られます。また、一部だけを改修する、何ならバグ修正をするだけの場合にも効果はあります。 ※ここでいう保守性は、いわゆる技術的負債を増やさず変更を容易にすることを指します。ログや監視などは指しません。

しかしこれらの勘違いにも無理はないと思います。こうした内容の解説書を読むと、何の役に立つかは説明されておらず、ただやり方だけが書かれていたりします。あるいは、要件定義から始めて全体を完全に理論通りやりきるという例しか載っていなかったりします。これでは、なかなかメリットもピンと来ませんし、理想の案件でしか取り込めないように見えてしまいます。

この隙間を埋めるチャレンジとして、理論から入るのではなく、まず現実的に取り入れやすくて効果が高いポイントを紹介しようと考えました。これなら、やったことがない現場での最初のチャレンジや、新人への説明にも使えます。順番にそれらを取り入れていくと、結果的にオブジェクト指向などの共通の理論に繋がっていることに気付き、理論にも興味が出る。という風になれば理想です。

初回の記事なのでだいぶ前置きが長くなりました・・・紹介するポイントよりも前置きの方が長かった気がします。具体的な話へ進みます。

ポイント1

仕様に出てくる要素に、日本語と英語で名前を付けましょう。コード上の関数・変数名を、その名前で統一しましょう。

具体例

ユーザー名一覧の情報を持った、DBがあるとしましょう。これを読む処理のメソッドと各処理のメソッドがあり、それらが共通して使うDB操作用のクラスがあるとします。

悪い例

これに対する、悪い例です。サンプルとして短く示すために、かなり極端な内容になってしまうのはご容赦。

//DBからユーザーを全て取得するメソッド
string[] ReadDB(){
    Database db;
    db.xxxx();
}

//ユーザーを追加するメソッド
void AddUserName(){
    Database db;
    db.xxxx();
}

//Databaseの読み書きをするクラス
class Database {
}

どうでしょう、名前がいまいちだなあ・・・と感じますかね?このコードでは、次のような問題点があります。

  1. AddUserName()で追加したものをReadDB()で読み込む事ができる、という対応関係が分かりづらい
  2. Read/Write(あるいはCRUD)とAdd/Remove/Getのような用語の組み合わせが混ざって使われている
  3. 日本語・英語共に、どのキーワードで検索しても、3つ全てが同時にヒットしない(改修対象を探す時に苦労する)
  4. ユーザー名だけを扱うDBに対して「Database」という一般名詞を付けてしまっているので、他にDBが増えると混乱する

これを見て、「アホか、こんなコード新人でも書かない」と思う人は、多分チーム全員がこの記事の内容をすでに理解しているので、スルーで良いと思います。

しかし、一からコードを書くと、意外にこういうコードを書いてしまう人は多いように思います。これは、「読み込みの処理」「書き込みの処理」をそれぞれコーディングしていき、その場でそれぞれの名前を考えているためだと思います。

良い例

まず、次のように名前を付けます。

仕様に出てくる要素・日本語名 英語名
ユーザー名 UserName
ユーザー名を保持するDB UserNameDatabase

そして、コードをそれに合わせて直します。ついでに、Add/Remove/Getのような用語の組み合わせも直します。

//ユーザー名を全て取得するメソッド
string[] GetUserNames(){
    UserNameDatabase db;
    db.xxxx();
}

//ユーザー名を追加するメソッド
void AddUserName(){
    UserNameDatabase db;
    db.xxxx();
}

//ユーザー名を保持するDatabaseの読み書きをするクラス
class UserNameDatabase {
}

どうでしょう、ちょっとした違いですが、だいぶコードの見通しが良くなったと感じませんか?悪い例に対応させて具体的に書くと、次の通りです。

  1. AddUserName()で追加したものをGetUserNames()で読み込める、という対応関係が名前から分かる
  2. Add/Remove/Getで統一されている
  3. 「ユーザー名」や「UserName」での検索で、関連するコードがヒットする(改修対象の見当を付けやすい)
  4. ユーザー名だけを扱うDBに対して「UserNameDatabase」という個別の名詞を付けているので、他のDBが増えても重複しない

(興味のある人だけ)理論的には

名前を付けて整理するのは、ドメイン分析、あるいはドメインモデリングの考え方です。(この場合はビジネス領域の用語ではなく、かなりコード寄りの名前付けですが)

ユースケース駆動開発においても、クラス図やシーケンス図の作成へ進む前に、まずはドメイン分析でこうした名前付けの整理を行うべきだという話が出ています。

またそもそも、「対象領域にあるものに名前を付けることで、対象領域の変化をコードへ反映しやすくする」というのは、オブジェクト指向という考え方そのものでもあります。

まとめ

長々と書きましたが、実のところ「日本語名と英語名を付けて、コード上で名前を統一しましょう」しか言っていません。現場で取り入れたり新人に教えるとしても、無理なくすぐに使えるのではないでしょうか。

そうでありながら、これはオブジェクト指向などのめんどくさそうな理論の実践でもあります。しかも、その中でも特に効果が高いものです。オブジェクト指向とかはそんなに面倒なものじゃなく、意外と実用的な話なんだな、と思ってもらえたら嬉しいです。

VRもくもく会を今週もやりました&その様子の写真

VRもくもく会を今週も開催しました。これは、「VRでのイベントというのも今後は選択肢に入ってくるのではないか、とにかくやってみて経験値を溜めてみよう」ということでやっています。2~3週間おきの定期開催ということで、もう1年以上続けています。

今日は皆なんとなくいろいろ作業をしてた感じだったので、成果発表コーナーは無しになりました。こういう日があってもいい、そんな感じの会です。

会場の様子を貼っておきます。

会場の様子1

会場の様子2

WiXSharpのカスタムアクションに、NuGetのライブラリを追加する方法

概要

WiXSharpというインストーラ作成用のライブラリの話です。以前の記事でも少し触れましたが、なかなか癖が強く、すぐにやれそうなことも意外と工夫が必要だったりします。

その中で、「NuGetでライブラリを追加したい」という、よく必要になる上に簡単にやれそうな作業で意外と工夫が必要だったので、今回はそのやり方を書きます。

最初に結論まとめ

  • ビルド先に出力されたdllの相対パスを、msi定義部分のDefaultRefAssembliesに手動で追記すればOKです
  • ちょっと工夫すると、自動でも追記できます
  • デフォルトのビルド先だとソリューション構成ごとにパスが変わってしまうので、ビルド後イベントで固定のパスにコピーしておくとやりやすいです

説明

問題の内容

「WiXSharp Managed Setup - Custom WPF UI (WiX4)」のプロジェクトを作成しました。

NuGetでライブラリを追加することで、カスタムアクションの実装に使うことができます。JSONを扱いたかったので、System.Text.Jsonを入れてみました。しかし、「カスタムアクションに実装したが、動かしてみるとDLLがロードできない」という厄介な動きになりました。こういった具合です。

System.IO.FileNotFoundException: ファイルまたはアセンブリ 'System.Text.Json, Version=8.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。

動きを観察してみると、WiXSharpによってビルドされたexeがmsiに埋め込まれ、msi実行時に一時フォルダへそのexeを展開し、実行しているようです。

するとこういうことになるので、DLLがロードできないのも道理です。

このexeを実行する一時フォルダへ、NuGetのDLLも出力させる必要があります。

解決方法

DefaultRefAssembliesの追加

WiXSharpでは、ManagedProjectクラスのインスタンスを作成して、そこにmsiの色々な設定をしていると思います。そこで、必要なDLLをDefaultRefAssembliesプロパティに追加します。すると、対象をmsiに埋め込んで、実行時に一時フォルダへ展開してくれます。

var project = new ManagedProject(ProductDefine.ProductName //省略

//省略

project.DefaultRefAssemblies = new List<string>
{
    //System.Text.Jsonの場合
    "System.Text.Json.dll",
    "Microsoft.Bcl.AsyncInterfaces.dll",
    "System.Buffers.dll",
    "System.Memory.dll",
    "System.Numerics.Vectors.dll",
    "System.Runtime.CompilerServices.Unsafe.dll",
    "System.Text.Encodings.Web.dll",
    "System.Threading.Tasks.Extensions.dll",
    "System.ValueTuple.dll",
};

NuGetと連携して自動でこれを作ってくれたりすると助かるんですが、どうやら手動での定義が必要なようです。

ただしこれだけだと、プロジェクトファイルからの相対パスなので、ファイルが見つからずにエラーとなります。ファイルが見つかるように、正しい相対パスで書く必要があります。次のような感じです。(以降、System.Text.Json.dll以外は省略します)

project.DefaultRefAssemblies = new List<string>
{
    "bin\Release\net481\win-x86\System.Text.Json.dll",
};

これで一応動きますが・・・しかしこれではパスにビルド構成が入ってしまうので、Debugビルドに変更しただけで失敗します。ちょっと不便です。

ビルド後イベントの追加

なのでもう一工夫して、ビルド後イベントで次のようにDLLを別フォルダにコピーすると良いです。

xcopy "$(TargetDir)*.dll" "$(ProjectDir)DependentAssembly\" /Y

そうすると、次のように参照できます。

project.DefaultRefAssemblies = new List<string>
{
    "DependentAssembly\System.Text.Json.dll",
};

これで、カスタムアクション内でNuGetで取得したDLLを使うことができます。

DefaultRefAssembliesの追加(応用)

WiXSharpのmsi生成処理は、ビルド完了後に動的に実行されます。(ビルド後イベントに元々入っている処理で実行されています)

そのため、ハードコーディングで1つずつ定義しなくても、ビルドで出力されたDLLを列挙して取り込むという手が使えます。ただし、WiXSharpが元々使っているDLLは除外する必要があります。すると、DefaultRefAssembliesを追加する部分で、次のようなコードを書けば実現できます。

var dependAsmDir = Path.Combine(Environment.CurrentDirectory, "DependentAssembly");
project.DefaultRefAssemblies = Directory.GetFiles(dependAsmDir, "*.dll")
    .Where(ValidateFileIsNotWiXSharpDefaultAsm)
    .ToList();

bool ValidateFileIsNotWiXSharpDefaultAsm(string filePath)
{
    //元々組み込まれているDLL。これを組み込むと二重に組み込むことになって無駄なので、対象から除外する。ネイティブDLLを渡すとDefaultRefAssembliesがエラーを出すので、それを回避する狙いもある。
    var defaultAsm = new[]
    {
        "Caliburn.Micro.Core.dll",
        "Caliburn.Micro.Platform.Core.dll",
        "Caliburn.Micro.Platform.dll",
        "mbanative.dll",
        "Microsoft.Xaml.Behaviors.dll",
        "WixSharp.dll",
        "WixSharp.Msi.dll",
        "WixSharp.UI.dll",
        "WixSharp.UI.WPF.dll",
        "WixToolset.Dtf.WindowsInstaller.dll",
        "WixToolset.Mba.Core.dll",
    };
    var fileName = Path.GetFileName(filePath);
    return defaultAsm.All(d => !d.Equals(fileName, StringComparison.OrdinalIgnoreCase));
}

解決方法まとめ

つまり、次の2つで解決可能です。

1,ビルド後イベントの最初に次の処理を追加

xcopy "$(TargetDir)*.dll" "$(ProjectDir)DependentAssembly\" /Y

2,WiXSharp向けコード(Program.cs)で、次のようにDefaultRefAssembliesを追加

var project = new ManagedProject(ProductDefine.ProductName //省略

//省略

var dependAsmDir = Path.Combine(Environment.CurrentDirectory, "DependentAssembly");
project.DefaultRefAssemblies = Directory.GetFiles(dependAsmDir, "*.dll")
    .Where(ValidateFileIsNotWiXSharpDefaultAsm)
    .ToList();

bool ValidateFileIsNotWiXSharpDefaultAsm(string filePath)
{
    //元々組み込まれているDLL。これを組み込むと二重に組み込むことになって無駄なので、対象から除外する。ネイティブDLLを渡すとDefaultRefAssembliesがエラーを出すので、それを回避する狙いもある。
    var defaultAsm = new[]
    {
        "Caliburn.Micro.Core.dll",
        "Caliburn.Micro.Platform.Core.dll",
        "Caliburn.Micro.Platform.dll",
        "mbanative.dll",
        "Microsoft.Xaml.Behaviors.dll",
        "WixSharp.dll",
        "WixSharp.Msi.dll",
        "WixSharp.UI.dll",
        "WixSharp.UI.WPF.dll",
        "WixToolset.Dtf.WindowsInstaller.dll",
        "WixToolset.Mba.Core.dll",
    };
    var fileName = Path.GetFileName(filePath);
    return defaultAsm.All(d => !d.Equals(fileName, StringComparison.OrdinalIgnoreCase));
}

まとめ

C#の開発では、NuGetで取得したDLLを使うというのはもはや息をするように自然にやっていることだと思いますが、WiXSharpでは意外に一工夫必要でした。以前の記事でも書いたように、条件が合えばとても便利なライブラリなので、こういう所で挫折せずに上手く使っていけると良いと思います。

Windowsインストーラ作成に、WiXSharpという選択肢もありますよ(ただしWiX Toolsetが使える人限定)

概要

Windowsインストーラを作ろうとした場合に、WiX Toolsetという有力な選択肢があります。そのWiX Toolsetをさらに作りやすくするWiXSharpというライブラリがあります。

しかしこのWiXSharpはなかなか癖が強く、前提条件がピタリはまると大きな力を発揮しますが、外れているとかえって苦労を呼ぶ場合があります。そのメリット・デメリットや、どういう人に向いているかというところを私の考えでまとめます。

最初に結論まとめ

説明

WiX ToolsetをWiXWindowsインストーラmsiと略記します

msiを本気で作ろうとすると、やはり最初に選択肢に挙がるのはInstallShieldでしょう。しかし同時に、「ライセンス料が高いので、もっと安く作れないか」という話が必ず挙がると思います。VisualStudioにもちょくちょくとインストーラ作成機能が入っては消えていますが、ただモジュールを入れるだけではなく複雑な処理をしようとすると力不足です。

そこで次に挙がってくる選択肢は、やはりWiX Toolsetではないでしょうか。素で作るには厄介すぎるmsiを、C#と独自のXMLをベースとしてかなり作りやすくしてくれるものです。しかし、それでもまだ「画面が作りづらい」「msi独自の定義が理解しづらい」などの問題は残ります。

これを解決するもう一つの選択肢に、WiXSharpがあります。しかし、概要にも書いたようになかなか厄介なので、それについて説明していきます。

メリット・デメリット

まず、メリットとデメリットをざっくりまとめます。その後に、詳しく説明してきます。

メリット

  1. msiの画面を、WPFxaml)で作成できる(XML的なmsi独自の何かではなく!)
  2. msiの全ての宣言をC#上で行うので、C#で実装を統一できる。カスタムアクションとの定義の共通化などがやりやすい
  3. msiの「そうはならんやろ」と皆が思っていたところを、いくつかラッピングして改善している

デメリット

  1. msiWiXの十分な知識があった上で、さらにWixSharpを学ぶ必要がある
  2. 完成されたライブラリというよりは、半完成品の製作キットという感じ

詳しく説明

メリット:msiの画面を、WPFxaml)で作成できる

これは最大のメリットだと思います。WiXであっても、画面作成については画面を定義しているmsiXMLをカスタマイズするような形で作る必要があって、専用のスキルが必要です。

その画面を、WPFで作ることができます。最初からCaliburn.Microが入っていて、MVVMやDIなども軽くなら使うことができます。これはつまり、Windowsアプリケーションを作っているスキルさえあれば、msiの画面を作る新たなスキルが必要ないという事です。インストーラは作ったアプリをインストールするためのものであり、インストーラのために新技術を習得するのは無駄が多いので、これはかなり助かります。

メリット:C#で実装を統一できる

WiXの場合はXMLで定義するような内容も、WiXSharpでは全てC#で定義できます。インストールするモジュールの一覧、バージョン、などの定義もC#であるため、これを例えばinternal staticのフィールドとして宣言しておけば、C#のカスタムアクションで同じ定義を使うこともできます。特にカスタムアクションで色々なことをやっている場合、これによるメリットが大きいと思います。

メリット:msiの不満点をいくつかラッピングして改善している

msiには、いまいち理解に苦しむところがいくつかあります。例えば、普通の開発者がイメージする「アップデート」を行うには、「ProductCode」を変更する必要があります。アップデートすると製品コードが変わる。直観的にピンと来ません。

そういったところを、C#のインターフェースに変えるに当たって、多少ラッピングして分かりやすくしています。もちろんmsiにも言い分は有ると思いますし賛否有ると思いますが、割と皆が思っていたことを反映してくれているように思います。

デメリット:msiWiXの十分な知識があった上で、さらにWixSharpを学ぶ必要がある

一見した印象では、msiWiXを隠蔽して使いやすくしたライブラリのように見えます。しかし、そうではありません。

あくまで、それらをC#で扱うためのアダプタを提供するものです。なので、WiXの何のアダプタなのかを理解して使わないと上手く行きません。何か調べ物をすると、「そのプロパティはWiXのこのXMLと同じだから、WiXのこの属性を設定すれば良いよ」みたいな、WiX知識前提の話が平気で出てきます。

ドキュメントもそういう感じで、「WiXと同名の定義については説明を省略する。独自の部分について説明する」というスタンスなので、そもそもWiXを知らないと説明が理解できません。

WPFについても同様で、「WPFのUserControlとして書けるようにして、共通インターフェースからこれらの情報を渡す仕組みを作った。それだけあれば、WPF使いなら分かるな?後は好きにやってくれ」という感じのスタンスです。WPFを知らない人は付いていけません。

デメリット:半完成品の製作キットという感じ

「インターフェースがきっちり決まっていて、中はブラックボックス」といった感じのライブラリではありません。何かサンプルと違うことをやろうとすると、構造を理解してカスタマイズするつもりで挑む必要があります。

例えば・・・NuGetでライブラリを追加するとカスタムアクション内で使用できますが、実行するとそのDLLがロードできません。 これは、WiXSharpのカスタムアクションは一時フォルダに展開して実行するという仕組みであるため、そこにDLLを持ち込む必要がある事を前提として理解する必要があります。それを理解していれば、NuGetのビルド出力をmsiに取り込む定義を足すことで、問題を解決できることが分かるはずです。といった具合です。

つまり、どういう人に向いているか

次のような人には向いていません。

そういう人がどうしてもWPFで画面を作りたい場合は、相当の覚悟をして採用する必要があります。WPFの画面にこだわりがなければ、まずは素のWiXを使ってみた方が良いと思います。

次のような人ならピタリとはまりますし、かなりのメリットを得られると思います。

まとめ

率直なところ、WiXSharpはよく考えずに触るとかなり痛い目を見る初見殺しのライブラリという印象はあります。しかし、WiXmsiWPFなどをよく理解している人が適切なケースで使うと、かなりの力を発揮します。これから取り組むプロジェクトに合うかどうかを慎重に検討した方が良いですが、使える場合はぜひ使ってみましょう。

PostgreSQLでSQLをコマンド実行する場合に、1回限りのパスワード認証をする方法

概要

PostgreSQLでプログラム上から単発のSQLを実行したい場合に、ファイルを経由せずに1回限りのパスワードを与える方法の話です。

最初に結論まとめ

コマンドプロンプトの/cのセッション内で環境変数を設定し、そのままコマンドも実行します。例えば次のようになります。

cmd /c "set PGPASSWORD=user1password&&psql.exe --username=user1 --dbname=postgres --command="CREATE DATABASE database1'""

話の背景

PostgreSQLpsql.exeを使って、--commandオプションや--fileオプションで単発のSQLを実行したい場合があります。手動で実行するのならば簡単で、インタラクティブでそのままパスワードを入力すれば済みます。

しかしこれをプログラム上から実行したい場合に、普通の手段だといったんファイルへユーザー名とパスワードを保存する必要があります。WindowsではACLを設定してファイルの内容を保護するようにという説明がありますが、できればファイル保存もしたくないところです。

ファイル保存せずに、1回限りのパスワードをプログラムから与えて実行する方法を考えたので、この記事を書きました。

説明と解決方法

psql.exeの仕様としては、ファイル保存の他にも環境変数を与える方法があります。環境変数の方は非推奨となっていますが、その理由はセキュリティにあるようです。ユーザー環境変数にしても、条件次第では他のユーザーから値を見ることができる可能性がある、と。

その点が問題なのであれば、コマンドプロンプトの実行セッションの間だけ存在する環境変数を使えば問題は無いはずです。ただ、コンソールアプリではないプログラムからコマンドを実行する場合、単発のコマンドでセッションが終わってしまうので、環境変数を与えられません。いったんバッチファイルを作成して実行すれば容易ですが、それではファイル保存しているので本末転倒です。

そこで、/cオプションの1つのセッションで、全体を""で囲みコマンドは&&で繋げる、という形で環境変数を与えたところ、上手く行きました。

まず、環境変数でパスワードを与える部分はこうなります。パスワードはuser1passwordだとします。

set PGPASSWORD=user1password

&&で続けてpsqlのコマンドです。例えば次のようになります。

set PGPASSWORD=user1password&&psql.exe --username=user1 --dbname=postgres --command="CREATE DATABASE database1'"

これを/cオプションの対象として、まとめるとこうなります。

cmd /c "set PGPASSWORD=user1password&&psql.exe --username=user1 --dbname=postgres --command="CREATE DATABASE database1'""

--commandオプションを例にしましたが、--fileオプションで.sqlファイルを与えることも同じように可能です。

cmd /c "set PGPASSWORD=user1password&&psql.exe --username=user1 --dbname=database1 --file="C:\Temp\Script.sql""

まとめ

PostgreSQLpsql.exeを、パスワード認証しながら単発で実行する方法が見つかりました。ベストな方法かどうかは分かりませんが、とりあえず私の要求は満たしていて問題はなさそうです。同じような物が必要になったら思いだしてみてください。

VRもくもく会を今週もやりました&その様子の写真

VRもくもく会を今週も開催しました。これは、「VRでのイベントというのも今後は選択肢に入ってくるのではないか、とにかくやってみて経験値を溜めてみよう」ということでやっています。2~3週間おきの定期開催ということで、もう1年以上続けています。

今日は成果発表コーナーで、こんな発表がありました。

  • mishizakiさん:明日のイベントの準備と、C# TokyoのSlackでやっているプログラムお題のプルリク取り込みなど。完了しました!
  • 私(suusanex):VRChatで使っているアバターをClusterにアップロードしようと変換してみた→変換したら、Clusterの方にミッションとかいう条件が増えててダメだった、また次回!

様子はこんな感じです。もくもく会の会場と、成果発表、それぞれ写真を貼っておきます。

もくもく会の会場

成果発表コーナー