白黒地帯

ゲームとか色々、Twitterに書ききれないことなど。

【Vintage Story】Modプロジェクトにテストプロジェクトを追加する

Fruitpressの改造モデル

Vintage Storyまた始めたついでにまたModまでいじりはじめました。
Modプロジェクトにユニットテストプロジェクトを追加してMod本体プロジェクトとVintage Storyのdll類をテストから参照させる方法を備忘のために記事にしておきます。
具体的にはVisual Studio 2022 + 公式テンプレート(Basic)を使用している場合に、ソリューションにMSTest単体テストプロジェクトを追加してModのユニットテストができるようにします。

目次

前提

コードModにしか関係のない話なのでコードMod開発の基礎知識はあるものとします。
具体的には実際に自分のコードModを作成していたり、以下のページのナビゲーションボックスのTutorials>Basicの内容まではやったよという人向けです。

https://wiki.vintagestory.at/index.php/Modding:Code_Mods

また繰り返しになりますが、Visual Studio 2022 + 公式テンプレート(Basic)を使用している場合に、ソリューションにMSTest単体テストプロジェクトを追加する手順です。
他のIDE・テンプレート・テストフレームワークでは適用できないか、異なる部分がある場合があります。

本編

テストプロジェクトを追加、Modプロジェクトを参照させる

まずModのソリューションをVisual Studioで開きます。

ソリューションエクスプローラーでソリューションを右クリック>追加>新しいプロジェクトを選択し、追加するプロジェクトの種類はMSTest Test Projectを選びます。

プロジェクト名は(Mod本体のプロジェクト名).Tests等にしておきます。
ここでプロジェクトができるフォルダがソリューションの外になっていることがあるのでその場合は中になるように直しておきます。

テストの.NETフレームワークの選択などは変えずにそのままで。

「作成」でテストプロジェクトが追加されるので、プロジェクト下の「依存関係」を右クリックし、テスト対象であるMod本体プロジェクトにチェック。

これでとりあえずMod内部で完結するクラスやメソッドなどのテストはできる…んですが

internalなクラスやメソッドなどをテストプロジェクトからテストできるように少し設定を加えます。
テストプロジェクトではなくMod本体の方のプロジェクトを右クリックし「プロジェクトファイルの編集」、プロジェクトファイルが開いたら一番下にあるItemGroupに以下を追加します。

    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
      <_Parameter1>$(MSBuildProjectName).Tests</_Parameter1>
    </AssemblyAttribute>

こんな感じになります。

テストプロジェクト名を(Mod本体のプロジェクト名).Testsとしている場合の値になっているのでテストプロジェクトの値が違う場合は<_Parameter1>の中を変更してください。

ここまでは至って普通なテストプロジェクトの追加ですね。
適当になにかテストして確認してみるとよいです。
(Vintage Story側にアクセスするものを使うとまだエラーになるはずなので関係ないものでテストする)

テストプロジェクトがVintage Storyのdllを参照できるようにする

まずテストプロジェクト側にVintage Storyのdllを参照させますが、数が多いので楽するために本体プロジェクト側からコピーすることにします。
本体プロジェクトを「プロジェクトファイルの編集」で開き、<Reference>でdllを参照している部分をまるごとテストプロジェクト側に同じようにコピーします。
この部分ですね。

  <ItemGroup>
    <Reference Include="VintagestoryAPI">
      <HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
      <Private>false</Private>
    </Reference>
    <Reference Include="VSSurvivalMod">
      <HintPath>$(VINTAGE_STORY)/Mods/VSSurvivalMod.dll</HintPath>
      <Private>False</Private>
    <!--中略-->
    <Reference Include="Microsoft.Data.Sqlite">
      <HintPath>$(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll</HintPath>
      <Private>False</Private>
    </Reference>
  </ItemGroup>

こんな感じになります。

最後にテスト時にVintage Story dllを参照できるようテスト設定ファイルでパスを指定します。
まずテスト設定ファイル.runsettingsをソリューションファイルを同階層に作ります。

中身は以下の通りにします。

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <MSTest>
    <AssemblyResolution>
      <Directory path="%VINTAGE_STORY%" includeSubDirectories="true"/>
    </AssemblyResolution>
  </MSTest>
</RunSettings>

これでVintage Story dll側のクラスやメソッドにアクセスできるはずです。
とはいっても(当然)ゲームが開始・初期化されていないため多くの機能は使えませんが、例えばGameMath等初期化不要の静的メソッドやTreeAttribute等データ構造は使用できます。

using Vintagestory.API.MathTools;

// 略

        [TestMethod]
        public void SmoothStepTest()
        {
            var d = GameMath.SmoothStep(0.5);
            Assert.AreEqual(0.5, d);
        }

// 略

.runsettingsは前述の通りのパスにこの名前で置けば自動検出してくれますが、好きな場所に置きたいときはメニューのテスト>実行設定の構成>ソリューション全体のrunsettingsファイルの設定から手動で指定することもできます。

.runsettingsについて
.runsettings ファイルを使用して単体テストを構成する - Visual Studio (Windows) | Microsoft Learn

おわりに

こんなところで。
あんまり真面目に言語や開発エコシステム自体をやる気がないので若干正確性に欠けるところはあるかもしれませんがご容赦。
とりあえず動くと思います()

おまけ:その他のプロジェクトセットアップ

FluentAssertions

テストプロジェクトにNugetからFluentAssertionsの追加(*.Should().*でアサーションしたいため。いらないならなくても可)

launchSettings.jsonを開発者ごとの環境に合わせられるように

デバッグ実行するにあたり実際にVintage Storyを起動することになりますが、開発者によりdataPathを分けていたり-oオプションですぐに起動するワールドを指定したりという細かい起動環境は変わってきます。
よってlaunchSettings.jsonのデフォルト構成はlaunchSettings.template.jsonとして分けておき、launchSettings.jsonはVCSから外します。
そしてビルド前イベントでlaunchSettings.jsonがなければlaunchSettings.template.jsonをコピーして作ります。
これで共通部分はVCSにのせつつ各開発者はlaunchSettings.jsonを自分の環境に合わせて編集できます。

launchSettings.template.json

{
  "profiles": {
    "Client": {
      "commandName": "Executable",
      "executablePath": "dotnet",
      "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"",
      "workingDirectory": "$(VINTAGE_STORY)"

    },
    "Server": {
      "commandName": "Executable",
      "executablePath": "dotnet",
      "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"",
      "workingDirectory": "$(VINTAGE_STORY)"
    }
  }
}

これはテンプレートのデフォと同じです。

launchSettings.json(自環境)

{
  "profiles": {
    "Client": {
      "commandName": "Executable",
      "executablePath": "dotnet",
      "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --dataPath \"$(VINTAGE_STORY_DATA_MOD_DEV)\" --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" -o tinycreative",
      "workingDirectory": "$(VINTAGE_STORY)"

    },
    "Server": {
      "commandName": "Executable",
      "executablePath": "dotnet",
      "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --dataPath \"$(VINTAGE_STORY_DATA_MOD_DEV)\" --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\"",
      "workingDirectory": "$(VINTAGE_STORY)"
    }
  }
}

自分はこんな感じで、通常プレイと環境を分けているのでdataPathを指定、-oでテスト用ワールドを直で起動としています。

ビルド前イベント

if not exist "Properties/launchSettings.json" (
    echo Creating launchsettings.json from template...
    copy "Properties\launchSettings.template.json" "Properties\launchSettings.json"
) else (
    echo launchsettings.json already exists. Skipping.
)

.gitignoreに以下を追加

launchSettings.json

modicon.pngをビルド先にコピーするよう設定

modicon.pngをプロジェクトに追加したらビルドアクションを「コンテンツ」にして「常にコピー」に。 これをやらないとデバッグ時にmodicon.pngが反映されないです。

dataPathを通常プレイと分離

launchSettings.jsonのところでも少し触れましたができればMod開発用のVintage Story環境は通常プレイと分けておきたいところ。
具体的にはVintage Story自体を通常プレイと別の場所にもう1つインストールし、さらにそれの起動時に--dataPathでdataPathとして使っていい適当な空のディレクトリを指定します。
このdataPath指定は、Modやセーブや設定が含まれるフォルダ(デフォルトはユーザーフォルダ以下のVintageStoryData)をカスタムで指定するものです。
(空のディレクトリを指定すると必要なファイルは初回起動時に自動作成されます)
これで起動したVintage StoryはdataPathを通常プレイと別にしているので、当然Modもセーブも設定も全く別環境になります。
後はdataPath以下に開発用のセーブなりmodなりを整えてあげればOKです。

これをやらないと困るのが通常プレイで自分の開発したModのリリース版を使う時。
Mod環境と分けていないと、Modをちょっと直したいな…と思って通常プレイから戻ってさあデバッグ…しようとしたときにリリースビルドをデバッグしようとしている旨の警告が出ます。
ビルドターゲットはDebugにしているのになぜ…と思うけども、それはリリース版のModをModフォルダから抜いていないから(ビルドしたデバッグ版Modとすでに入っているリリース版Modをどちらも読み込もうとしてしまう)。
一旦リリース版Modを抜いてMod開発して終わったら新たなリリース版に戻して…という操作が必要になるんですが…紛らわしい!
万が一通常プレイに戻ったときにModを入れ忘れてそのままセーブしてしまうと面倒くさい。

この問題はdataPathを分ければ解決する…ということで分けたほうが便利です。

おまけ:Vintage Story Mod開発でその他役立つページなど

コードMod関連は以下のページのナビゲーションボックスから一通り基礎・チュートリアルの内容にアクセスできる。

https://wiki.vintagestory.at/index.php/Modding:Code_Mods

ただなぜかリリース・パッケージングについてはこのページにしか記載がない。
(「CakeBuild」を実行するだけといえばそうなのだが)

https://wiki.vintagestory.at/index.php/Modding:Setting_up_your_Development_Environment

デフォルトではwikiの検索にModding関連は出てこないのだがここの検索ボックスからModding関連もオンにした検索がすぐできる。

https://wiki.vintagestory.at/index.php/Modding:Getting_Started

ここの一番下のナビゲーションボックスからModding関連ページを一覧できる。

https://wiki.vintagestory.at/index.php/Modding:Navigation_Box_Updates

…といいつつコードMod系はなぜか入ってないのでここのページのナビゲーションボックスから探す。

https://wiki.vintagestory.at/index.php/Modding:Code_Mods

ナビゲーションボックスにない個別ページもあるので結局普通の検索エンジンでVintage Story modding ~で探したほうがいいかもしれない。

重要そうなページ:

サーバークライアント間での同期について

https://wiki.vintagestory.at/index.php/Modding:Server-Client_Considerations

効率的なMod開発のために使えるツールなど

https://wiki.vintagestory.at/index.php/Modding:Modding_Efficiently

Vintage Story中.ediコマンドで拡張デバッグインターフェースを有効にするとアイテムのコードがゲーム中で見れたりしてとても便利。