Modlet

Last-modified: 2025-05-22 (木) 13:02:58

Modletとは

Modletとは、ゲームファイル本体を上書きしない小さなModのことです。
Alpha17から導入されました。
(Modletの仕組みが導入される前は、Mod製作者たちはゲームファイル自体を編集して配布していたため、競合やゲームバージョンのアップデートで問題が起きやすくなっていました。Modletはゲームファイル本体を直接書き換えたりしないため、そういった問題が起きにくくなっています)

Modletの導入方法

基本的に、Modsフォルダに任意のModletを放り込むだけで導入できます。
すでにセーブデータがある環境に導入する場合は、導入前に必ずバックアップをとってください。
Mod Launcher を利用する場合は、ManageModletsからModletを管理することができます(プロジェクト別管理のようなことも可能です)。

Modletの詳細

形式は公式・非公式の物が存在し、簡便さや自由度の違いからすみ分けされています。
ここでは広く使われている公式の方法を紹介します。

 

Modletの形式

公式基準のModletは、7 days to dieのホームディレクトリ直下のModsディレクトリに、それぞれ固有のフォルダ名で保存され、その中には必ずModInfo.xmlが入っています。

Mods/MyMod/ModInfo.xml
Mods/YourMod/ModInfo.xml

ModInfo.xml

以下の内容で作るか、ここからダウンロードして入れましょう。
NameはそれぞれのModlet固有である必要があり、同じものは無視されます。

~~展開~~

折り畳み

<?xml version="1.0" encoding="UTF-8" ?>
<xml>
	<ModInfo>
		<Name value="test" />
		<Description value="test dayo" />
		<Author value="Yamada Taro" />
		<Version value="1.0.0" />
	</ModInfo>
</xml>
 

ModInfo.xmlはa21からフォーマットがV2に変わりました。
a21では旧フォーマットのV1でも動作はしますが、ワーニングが出ます。
v1.0からはV1は使用できなくなっています。

~~展開~~

折り畳み

<?xml version="1.0" encoding="UTF-8" ?>
<xml>
	<Name value="test" />
    <DisplayName value="V2 test" />
	<Version value="1.0.0.0" />
	<Description value="test dayo" />
	<Author value="Yamada Taro" />
    <Website value="http://test.com" />
</xml>

Name:【必須】世界で固有の内部名となるような名前を付けます。英数字、アンダーバー、ハイフンのみ使用可能となりました。
DispkayName:【必須】表示名で、ゲーム中に表示される場合に使われます。
Version:【必須】数字とピリオドのみで、メジャーバージョン・マイナーバージョン・ビルド番号・変更番号のように付けることが推奨されています。
ビルド番号と変更番号、または変更番号のみの省略は可能です。
Description:【省略可】Mod/Modletの説明です。
Author:【省略可】作成者の名前です。複数人で作成している場合は列挙しましょう。
Website:【省略可】Mod/Modletの説明や公開サイトのURL

 

公式の方法で可能なことは、Config内のXMLの操作と一部Assetの追加です。
Assetは7 days to dieのホームディレクトリ以下なら場所を問いませんが、XMLの操作はModletフォルダ内のConfigsに、変更するXMLと同じ名前で入っています。

Mods/MyMod/Configs/items.xml
Mods/MyMod/Configs/XUi/windows.xml
 

XMLの操作

以下の8個の操作でXMLを変更することができます。

a18からLocalizationが変更できるようになりました。

  • append 子要素の後尾に追加
  • prepend 子要素の先頭に追加
  • insertAfter その要素の一つ後として追加
  • insertBefore その要素の一つ前として追加
  • remove その要素を消す
  • set 今ある要素/属性の代わりに設定(存在しない属性を足すことはできません)
  • setattribute 属性を追加
  • removeattribute 属性を削除(a18から使用可)
~~append~~

append折り畳み
元XML

<items>
  <item name="1"/>
</items>

XML操作XML

<configs>
  <append xpath="/items">
    <item name="2"/>
  </append>
</configs>

出力XML

<items>
  <item name="1"/>
    <item name="2"/>
</items>
~~prepend~~

prepend折り畳み
元XML

<items>
  <item name="1"/>
</items>

XML操作XML

<configs>
  <prepend xpath="/items">
    <item name="2"/>
  </prepend>
</configs>

出力XML

<items>
    <item name="2"/>
  <item name="1"/>
</items>
~~insertAfter~~

insertAfter折り畳み
元XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5"/>
  </item>
</items>

XML操作XML

<configs>
  <insertAfter xpath="/items/item[@name='1']/property[@name='A']">
    <property name="C" value="7"/>
  </insertAfter>
</configs>

 ※itemが1つしか無い場合、又は全てのitemに適用する場合は[@name='1']を省略できます。
  詳細はXPathの項を参照。

出力XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="C" value="7"/>
    <property name="B" value="5"/>
  </item>
</items>
~~insertBefore~~

insertBefore折り畳み
元XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5"/>
  </item>
</items>

XML操作XML

<configs>
  <insertBefore xpath="/items/item[@name='1']/property[@name='B']">
    <property name="C" value="7"/>
  </insertBefore>
</configs>

 ※itemが1つしか無い場合、又は全てのitemに適用する場合は[@name='1']を省略できます。
  詳細はXPathの項を参照。

出力XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="C" value="7"/>
    <property name="B" value="5"/>
  </item>
</items>
~~remove~~

remove折り畳み
元XML

<items>
  <item name="1"/>
  <item name="2"/>
</items>

XML操作XML

<configs>
  <remove xpath="/items/item[@name='1']"/>
</configs>

出力XML

<items>
  <item name="2"/>
</items>
~~set~要素の置き換え

set(要素の置き換え)折り畳み
元XML

<items>
  <item name="1"/>
</items>

XML操作XML

<configs>
  <set xpath="/items">
    <item name="2"/>
  </set>
</configs>

出力XML

<items>
     <item name="2"/>
</items>
~~set~属性の置き換え

set(属性の置き換え)折り畳み
元XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5"/>
  </item>
</items>

XML操作XML

<configs>
  <set xpath="/items/item[@name='1']/property[@name='B']/@value"> 10 </set>
</configs>

 ※itemが1つしか無い場合、又は全てのitemに適用する場合は[@name='1']を省略できます。
  同様に、全てのpropertyに適用する場合は[@name='B']を省略できます。
  詳細はXPathの項を参照。

出力XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="10"/>
  </item>
</items>
~~setattribute~~

setattribute折り畳み
元XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5"/>
  </item>
</items>

XML操作XML

<configs>
  <setattribute xpath="/items/item[@name='1']/property[@name='B']" name="condition"> walk </setattribute>
</configs>

 ※itemが1つしか無い場合、又は全てのitemに適用する場合は[@name='1']を省略できます。
  同様に、全てのpropertyに適用する場合は[@name='B']を省略できます。
  詳細はXPathの項を参照。

出力XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5" condition="walk"/>
  </item>
</items>
~~removeattribute~~

removeattribute折り畳み
元XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5" condition="walk"/>
  </item>
</items>

XML操作XML

<configs>
  <removeattribute xpath="/items/item[@name='1']/property[@name='B']/@condition"/>
</configs>

 ※itemが1つしか無い場合、又は全てのitemに適用する場合は[@name='1']を省略できます。
  同様に、全てのpropertyに適用する場合は[@name='B']を省略できます。
  詳細はXPathの項を参照。

出力XML

<items>
  <item name="1">
    <property name="A" value="3"/>
    <property name="B" value="5"/>
  </item>
</items>

操作を行う場所は、XPathで指定します。

 

IF構文

IF構文はv1.0から実装された機能で、色々な条件によりxml操作を有効/無効にする機能です。
C言語の#if~#elseif・・・#else~#endifマクロに似ています。
これを使うと、バニラのバージョンごとに動作を変えたり、大型Modごとに動作を変えたり、他のModletとの連携などができます。
基本的な構文は以下のようになります。

<conditional evaluator="判断する場所">
  <if cond="条件1">
    条件1で有効にしたいxmlの操作
  </if>
  <if cond="条件2">
    条件1が成り立たず、条件2で有効にしたいxmlの操作
  </if>
  <if cond="条件3">
    条件1・2が成り立たず、条件3で有効にしたいxmlの操作
  </if>
  :
  <else>
    上記に記述した条件以外で有効にしたいxmlの操作
  </else>
</conditional>
 

要素conditional

ifは必ずconditionalで囲む必要があります。
conditionalには属性evaluatorがあり、ifの判断を行う場所を指定することができます。

host:サーバー側でのみ判断し、それによって生成されたxmlファイルをクライアントに送ります。
属性evaluator自体を省略するとhost扱いになるので、通常は省略でいいでしょう。
client:サーバーとクライアントでそれぞれ判断します。
特殊な実装を行う場合のみ使用します。

但し、xpathのルート(最上位)に記述した場合はclientと記述してもhostとして扱われます。

 

要素if

ifは属性condに指定した条件の判定結果で<if>~</if>で囲まれたxmlを有効にします。
条件はand/orで複数記述することができます。
条件判定としては、以下のものが使えます。

>= 左が右と同じかそれより大きいか xml中で使う場合は、&gt;=と記述します
<= 左が右と同じかそれより小さいか xml中で使う場合は、&lt;=と記述します
> 左が右より小さいか xml中で使う場合は、&gt;と記述します
< 左が右より小さいか xml中で使う場合は、&lt;と記述します
== 左と右が同じか
!= 左と右が異なるか
<> 左が右より大きいか小さいか xml中で使う場合は、&lt;&gt;と記述します

ifは複数記述可能で、基本的な構文で示したように複数並べて記述できます。

 

要素else

elseは直前までに同じレベルで書かれたifの条件全てに当てはまらない場合に<else>~</else>で囲まれたxmlを有効にします。
必要ない場合は省略できます。

 

条件で使える関数

mod_loaded(s1)
他のModletが導入されているかを確認します。
s1に入るのは対象のModlet名で、対象のModletのModInfo.xmlに記述されている、要素Nameの属性valueに指定されている値になります。
mod_version(s1)
他のModletのModInfo.xmlの要素Versionのvalueに指定された値を取得します。
s1に入るのは対象のModlet名で、対象のModletのModInfo.xmlに記述されている、要素Nameの属性valueに指定されている値になります。
比較するときは、version()関数を使って比較します。
game_version()
バニラのバージョンを取得します。
メジャーバージョン、マイナーバージョン、ビルド番号が返ります。
比較するときは、version()関数を使って比較します。
version(s1,s2,s3)
バージョンを比較するときに使用します。
s1,s2,s3には比較したいバージョンのメジャーバージョン、マイナーバージョン、リリース番号orビルド番号を指定します。
s3が不要な場合はs3を、s2が不要な場合はs2とs3を省略できます。
例)
game_version() == version(1,2,3) ゲームのバージョンがv1.2(b3)か判断します。
game_version() == version(1,2) ゲームのバージョンがv1.2かを判断します。ビルド番号は比較しません。
game_version() == version(1) ゲームのバージョンがv1かを判断します。マイナーバージョン・ビルド番号は比較しません。
game_loaded()
マルチで参加するゲームをサーバーからロードし終わったかを確認します。
サーバーの情報を取得するような関数を使用する場合に併用します。
属性conditionalをclientに設定して使用しましょう。
返ってくる値は、bool値(trueかfalse)になります。
serverinfo(s1)
接続しているサーバー情報を取得します。
属性conditionalをclientに設定し、game_loaded()を併用して、接続したゲームのロードが終了している状態で使用しましょう。
s1にはserverconfig.xmlの要素propertyの属性Nameで指定されている値を指定します。
返ってくる値は、指定したプロパティにより、数値/文字列/bool値(trueかfalse)になります。
gamepref(s1)
クライアント側のオプション設定を取得します。
注意)プレイ中にオプション設定を変更した場合は、次回ログイン時まで反映されません。
s1にはXUi_Menu\windows.xmlで使用されている、オプション値を格納している内部名を指定します。
返ってくる値は、指定したプロパティにより、数値/文字列/bool値(trueかfalse)になります。
time_minutes()
サーバーまたはクライアントの分を取得します。
属性conditionalをhostかclientに設定することで取得先が決まります。
奇数分は取得できないので、判定方法に注意しましょう。
返ってくる値は、数値(0~58)になります。
event(s1)
ゲームがイベント期間中かを返します。
s1にはevents.xmlに記述された要素eventの属性nameで指定されている値を指定します。
そのイベント中であればtrue、そうでなければfalseが返ります。
例)
event("easter") イースターの日(春分の後の最初の満月の次の日曜日)の前後2日以内であればtrueが返ります。※バニラの場合
event("christmas") 12月25日から翌1月2日であればtrueが返ります。※バニラの場合
event("halloween") 10月31日から7日後までであればtrueが返ります。※バニラの場合
event("thanksgiving") 感謝祭の4日前から8日間までであればtrueが返ります。※バニラの場合
注意)感謝祭はおそらく米国の11月の第4木曜日が採用されると思われます
 

補足説明

Modletによるxmlの操作が全て完了した状態を見たい場合は、Modletを入れて起動したゲームのセーブデータフォルダ内の以下のフォルダに格納されています。

Saves\ワールド名\ゲーム名\ConfigsDump

ワールド名はゲームを続けるときのセーブデータ一覧の対象のゲームの枠の左下に表示される名前です。
※ランダムワールドの場合はシード値ではありません。シード値から生成される名前です。
固定マップの場合は選択した固定マップ名と同じです。

ゲーム名はゲームを続けるときのセーブデータ一覧の対象のゲームの枠の左上に表示される名前です。

生成されたxmlファイルの中には、どのModletによる操作か判るようにコメントが入ります。
イメージした通りの編集になっているかを確認することができます。
実際のプレイ時は、このxml群の内容でゲームが動作します。

編集済のxmlファイルの構成は、7 Days to DieのインストールフォルダのDATA\Configsフォルダの中身と同じです。
※このフォルダにLocalization.txtは含まれません。
※変更済のxmlはワールドにINするまでの間に生成されます。
※Modletでエラーが出ている場合は編集に反映されない場合があります。

 

XPath

XPathはXML内のとある位置を指し示す世界標準の方法です。
専門的な言葉を使えば、XMLをツリーとみなしてルートからたどるパスです。

基本

基本的に<>で囲まれたテキストの初めに来る、要素名を/(スラッシュ)で区切ってたどっていきます。

<items>
  <item name="meleeToolStoneAxe"/>
</items>

上の例で<item name="meleeToolStoneAxe"/>を指定する場合は、/items/itemでたどることができます。

 

ただし、次のような場合、/items/itemをたどると2個のitem両方が指定されます。

<items>
  <item name="meleeToolStoneAxe"/>
  <item name="meleeToolFireaxeIron"/>
</items>

要素名だけでたどれない時は、条件で絞り込むことができます。
条件は[]のカッコで囲みます。単純な例は、[1]のような数字で、何番目か指定する方法です。
例えば、/items/item[1]<item name="meleeToolStoneAxe"/>を指定できます。

ただし、この方法では新しいアイテムが追加されたり、順番が変わるだけで動かなくなります。

<items>
  <item name="meleeClubWood"/>
  <item name="meleeToolStoneAxe"/>
  <item name="meleeToolFireaxeIron"/>
</items>

この場合、<item name="meleeClubWood"/>の方が指定されてしまいます。

 

代わりに、属性を使った条件の方がうまくいくことが多いでしょう。
XPathでは属性の名前には@を付けて、@nameというように書きます。
/items/item[@name='meleeToolStoneAxe']と書けば、一通りに指定することができます。

 

条件で使える関数

last()
今の要素の中で最後の要素の番号を返します。
[last()]とすれば、一番最後の要素を指定できます。
starts-with(s1,s2)
文字列s1が文字列s2で始まる場合、真になります。
[starts-with(@name,'meleeTool')]とすれば、nameがmeleeToolで始まる要素を指定できます。
contains(s1,s2)
文字列s1が文字列s2を含む場合、真になります。
[contains(@name,'Iron')]とすれば、nameにIronを含む要素を指定できます。
position()
要素の番号を返します。
[position() &lt; 3]とすれば、3番目より後の要素を指定でき、
[position() &gt; 3]とすれば、3番目より前の要素を指定できます。
(&lt;は<の、&gt;は>のxml用の書き換えです。)
string-length(str)
文字列strの文字数を返します。
[string-length(@id)=1]とすれば、idが一桁の要素を指定できます。
substring(str,num)
文字列strの中で、num文字目から先の文字列を返します。
[substring(@name,string-length(@name) - string-length('Radiated') +1) = 'Radiated']とすれば、nameがRadiatedで終わる要素を指定できます。
(ends-withの代わり)
and
前の条件と後ろの条件が両方正しい場合真になります。
[starts-with(@name,'melee') and contains(@name,'Iron')]とすれば、nameがmeleeで始まって、Ironを含む要素を指定できます。
ただし、[starts-with(@name,'melee')][contains(@name,'Iron')]のように、2重に条件を使うことでも同じように指定できます。
or
前の条件と後ろの条件のどちらかが正しい場合真になります。
[starts-with(@name,'melee') or starts-with(@name,'gun')]とすれば、nameがmeleeかgunで始まる要素を指定できます。
not(条件)
条件を反転します。
[starts-with(@name,'gun') and not(ends-with(@name,'Admin'))]とすれば、nameがgunで始まり、終わりがAdminではない要素を指定できます。
 

省略

*
名前に関係なく要素を指定します。
名前を指定しても意味がない時や、複数の経路を同時にたどる時に使えます。
//
/の代わりに使うと、その要素直下だけでなく、それ以下の全ての要素の中から探します。
 

ローカライゼーション

Alpha18のローカライゼーションの操作はModletフォルダ内のConfigsに、Localization.txtという名前で入っています。

Mods/MyMod/Configs/Localization.txt
 

このファイルはテキストファイルで、カンマで区切るcsv形式で書かれています。
テキストとは言え、UTF-8を使っていますので、UTF-8の使えるテキストエディタ等で編集する必要があります。

 

作成する場合は、まずオリジナルのLocalization.txtの1行目をコピーし、2行目以降に追加したい内容を記述します。

 

1行目はアイテムの分類や各種言語の項目名が入っています。
それにのっとり説明を追加していきます、
1行が大変長いですが、Keyには内部名を英語で記述、englishより前までは、オリジナルファイルの記述を参考にします。
※内部名とは、Items.xmlやBlocks.xml等で記述した名前です

 

englishから後は各言語による記述になります。
途中で改行を入れることはできません。
※説明文の表示時に改行を入れたい場合は、改行したい位置に\nを入れます。
※説明文の表示にカンマを含む場合は、その言語の部分の文全てをダブルクォーテーションで囲む必要があります。
※説明文の表示にダブルクォーテーションを含む場合は、その言語の部分の文全てをダブルクォーテーションで囲み、文中のダブルクォーテーションは""と2つ続けて記述する必要があります。
※説明文の表示に<や>を含む場合は、それぞれ<と>と記述する必要があります。
※説明文の文字の色を変えたい場合、変えたい部分を[RRGGBB]と[-]で囲む必要があります。RRGGBBは赤緑青の並びで色の明るさを16進数2桁で記述します。

 

内容はアイテム名・ブロック名の名前や説明文、クエストの説明文などになります。
アイテムやブロックの説明文のKeyは、アイテム名のKeyの後ろにDescを付けます。しかし、items.xml/blocks.xml内で当該アイテムに DescriptionKey プロパティを特別に設定した場合はこの限りではありません。

 

各言語の部分はenglish以外は省略でき、その場合は英語の記述が引用されます。
カンマは省略できません。

 

他言語の記述をしない場合は1行目を含め省略して書くことができます。

Key,File,Type,NoTranslate,english,japanese
内部名,items,Item,,英名,日本名
内部名Desc,items,Item,,英文説明,日本文説明

※アイテムを追加する場合の例
※項目NoTranslateは、英語でのみ表示する設定ですので、ちゃんと日本語で表示されるように入れておきましょう。(設定値には何も入れません)

 

詳しくはオリジナルのLocalization.txtを参考にしてください。

既存のアイテムに日本語を追加する場合

日本語表示されない物に日本語を追加するだけであれば、Localization.txtの中身を大幅に省略できます。
具体的には、内部名と日本語だけあればOKです。
※項目NoTranslateは念のために入れておきましょう。

Key,NoTranslate,japanese
内部名,,日本語説明文

間違った日本語を修正する場合も同様です。
但し、追加するアイテムがある場合などは混在ができませんので、そちらに合わせる必要があります。

アイテムアイコン

詳しくは、アイコン追加

Harmony

Alpha20 から導入された機能です。ゲーム本体のバイナリ (Assembly-CSharp.dll) にパッチを与えたり、独自クラスを定義したりすることが可能となります。
かつて大型 Mod/オーバーホール Mod と呼ばれたインストール方法を過去のものにします。ディレクトリ 7DaysToDie_Data を上書きする必要がなくなると同時に、アンインストール時に整合性チェックを実行する必要もなくなりました。手軽にオーバーホールを楽しむことが可能です。
v1.0からModsフォルダの中にThe Fun Pimpsが調整したHarmonyのDLLが提供されるようになりました。
XML を書き換えるだけでは実現し得なかった実装を、このチュートリアルで試してみてください。
Harmony を利用した場合、EAC を通して起動することが不可能となります。 EAC をオフにして動作確認を行ってください。

より詳しい情報は、Harmony 公式サイト https://harmony.pardeike.net/ (English) にアクセスしてください。

 

Step by step coding tutorial

  • .NET Framework コンパイル/ビルド環境を整える
    • Visual Studio Community を利用する
    • Visual Studio Code を利用する
  • 必要最低限のテンプレートを準備する
  • 実際にコードを書き換える
  • Modlet に同梱する

Reference

  • Prefix
  • Postfix

Step by step coding tutorial

この例で紹介するソースコードは Alpha20.6(b9) 時点の情報をもとにしており、CC0-1.0 ライセンスが適用されます。

Visual Studio Community

Windows をお使いの皆様は、Visual Studio Community を利用する選択肢が最も有力です。

インストーラを起動して、.NET デスクトップ開発にチェックを入れます。インストールされる個別のコンポーネントの中に、.NET Framework 4.x SDK と Targeting Pack が含まれていることを確認してください。執筆時点の最新版は 4.8.1 です。
インストールにはしばらく時間がかかります。再起動を促された場合、無理して続行せず再起動をして仕切り直しましょう。

Visual Studio を起動して、新しくプロジェクトを作成します。以下の設定を確認してください:

  • プロジェクト テンプレートClass Library(.NET Framework) を選択すること
  • フレームワーク バージョンはインストールしたバージョンを選択すること

.NET Core や WPF .NET Framework 等をターゲットにコンパイルされた DLL は、7DTD に認識されません。 必ずプレーンな .NET Framework を選択してください。

ソリューションの名前は TestProject とします。

ソリューションが作成されたら、必要に応じて参照を追加してください。必須の参照は 0Harmony.dll と Assembly-CSharp.dll です。
Steam デフォルト設定では以下のパスに存在します:
%PROGRAMFILES(X86)%\Steam\steamapps\common\7 Days To Die\7DaysToDie_Data\Managed\

Visual Studio Code

macOS や Linux ディストリビューションをお使いの皆様は、Visual Studio Code を利用する選択肢が最も有力です。

加えて、以下の SDK を入手してください:

SDK がインストールされると、mcs コマンドが使用可能になります。使用できない場合、お使いの OS に対応していない可能性があります。OS を最新版に更新する、もしくは他の無料の Linux ディストリビューションを利用する、Windows を利用する等、回避策を検討してください。

Visual Studio Code を起動して、ワークスペースを準備します。ディレクトリ名は TestProject とします。

TestProject ディレクトリに .vscode という名前のディレクトリを配置し、tasks.json という名前のファイルを準備します。
もしくは、ショートカットキー Ctrl + Shift + B (macの場合は ⌘Cmd + ⇧Shift + B) から、テンプレート Others のタスクを構成してください。

□ .vscode/tasks.json

{
    "tasks": [
        {
            "label": "compile",
            "type": "shell",
            "command": "mcs",
            "args": [
                "-target:library",
                "-warn:0",
                "-o+",
                "-unsafe",
                "-r:${userHome}/.steam/steam/steamapps/common/7 Days To Die/7DaysToDie_Data/Managed/0Harmony.dll",
                "-r:${userHome}/.steam/steam/steamapps/common/7 Days To Die/7DaysToDie_Data/Managed/Assembly-CSharp.dll",
                "-r:${userHome}/.steam/steam/steamapps/common/7 Days To Die/7DaysToDie_Data/Managed/UnityEngine.CoreModule.dll",
                "-langversion:latest",
                "-out:${workspaceFolderBasename}.dll",
                "**/*.cs",
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

0Harmony.dll や Assembly-CSharp.dll までのパスは、ご自身の環境に置き換えてください。必要に応じて、参照を追加してください。

必要最低限のテンプレートを準備する

Harmony に対してフックを仕掛けるには、インターフェース IModApi を実装したクラスが必要です。

□ Harmony/TestProject_HarmonyInit.cs

using HarmonyLib;
using System.Reflection;
using UnityEngine;

public class TestProject_HarmonyInit : IModApi
{
    public void InitMod(Mod _instance)
    {
        Debug.Log($" Loading Patch: {GetType()} Mod: {_instance.ModInfo.Name}");
        var harmony = new Harmony(GetType().ToString());
        harmony.PatchAll(Assembly.GetExecutingAssembly());
    }
}

このコードにより、7DTD 側に Harmony を実装している Modlet であると宣言することになります。
ビルドが正常終了することを確かめてください。

よくあるエラー

出てきたエラーコード error CSXXXX を検索すれば、だいたい docs.microsoft.com のトラブルシューティング ページに案内されます。ここでは、陥りやすいエラーとその回避策を記述します。

 
error CS1525: Unexpected symbol `***'(, expecting `***' or ...)

文法エラーです。C# の文法に則っているか確認してください。

 
error CS0117: `***' does not contain a definition for `***'

定義が不足しています。対象のクラスや名前空間に定義が含まれているかどうか、キーワードが正しいかどうか確認してください。

 
error CS0246: The type or namespace name `***' could not be found. Are you missing an assembly reference?

参照が不足しているため、クラスや名前空間が利用できない状態です。Managed ディレクトリの中から必要な DLL を参照に追加してください。

 
error CS0006: Metadata file `/.../7 Days To Die/7DaysToDie_Data/Managed/***.dll' could not be found

参照までのパスが間違っています。パスが正しいかどうか確認してください。

 
error CS1070: The type `***' has been forwarded to an assembly that is not referenced. Consider adding a reference to assembly `***, Version=X.X.X.X, Culture=***, PublicKeyToken=***'

参照の実体は別のアセンブリに含まれています。サジェストされた DLL を参照に追加してください。

実際にコードを書き換える

この例では、ツールベルトを15個に増やします。

□ Harmony/FifteenSlotToolbelt_Inventory.cs

using HarmonyLib;

public class FifteenSlotToolbelt_Inventory
{
    /**
     * <summary>New slot number for Toolbelt</summary>
     */
    public const int INVENTORY_SLOT_NEW = 15;

    [HarmonyPatch(typeof(Inventory))]
    [HarmonyPatch("get_PUBLIC_SLOTS")]
    public class FifteenSlotToolbelt_harmony_Inventory_get_PUBLIC_SLOTS
    {
        public static void Postfix(ref int __result)
        {
            __result = INVENTORY_SLOT_NEW;
        }
    }
}

解説

HarmonyPatch 属性を駆使して、既存のコードを書き換えます。

10行目 [HarmonyPatch(typeof(Inventory))]
クラス Inventory に対してコード書き換えを行う宣言です。
11行目 [HarmonyPatch("get_PUBLIC_SLOTS")]
メソッド get_PUBLIC_SLOTS に対してコード書き換えを行う宣言です。

Assembly-CSharp.dll の中に、int 型の Inventory.PUBLIC_SLOTS というプロパティが存在します。この値をもとにして、ツールベルトの数が決まります。
C# では中間言語 (以下: IL) にコンパイルされる時点で、プロパティの getter は get_${PROPERTY_NAME} という名前のメソッドに変換されます。

14行目 public static void Postfix(ref int __result)
対象のメソッド内の処理が完了した後、このメソッドに記述されているコードが実行されます。
16行目 __result = INVENTORY_SLOT_NEW;
引数を書き換えます。

引数 __result はオリジナルの戻り値を受け取ります。今回は戻り値を書き換えるため、ref キーワードを設定して戻り値に影響を及ぼさせます。
引数 __result にはツールベルトの数が設定されています。処理して返されたオリジナルの値がどんなものであろうと、INVENTORY_SLOT_NEW (=今回は15) に書き換えます。

 

ビルドが正常終了することを確かめてください。

また、IL の中身を覗いてみたい衝動に駆られた方は、ILSpy や dnSpy を利用してください。

続いて、このままではツールベルトの HUD が2行になるにも関わらず、残りの5つを選択することができなくなります。見た目を改善するため、Config/XUi/windows.xml を書き換えます。

□ Config/XUi/windows.xml

<modconfig>
    <set xpath="/windows/window[@name='windowToolbelt']/@width">1128</set><!-- ツールベルト ウィンドウサイズ -->
    <set xpath="/windows/window[@name='windowToolbelt']/@pos">-575,92</set><!-- ツールベルト ウィンドウをどこに表示させるか、親コンポーネントからの相対位置 -->
    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@controller='Toolbelt']/grid[@name='inventory']/@cols">15</set><!-- スロットは15個 -->

    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@controller='Toolbelt']/grid[@name='inventory2']/@visible">false</set><!-- 2行目は非表示 -->

    <set xpath="/windows/window[@name='windowToolbelt']/rect/sprite[@fill='{xp}']/@width">1125</set><!-- 大きくなったウィンドウサイズに経験値インジケータを合わせる -->

    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@stat_type='Food']/@width">565</set><!-- 食料インジケータも合わせる -->
    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@stat_type='Food']/filledsprite[@name='BarContent']/@width">563</set>

    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@stat_type='Water']/@width">565</set><!-- 水分インジケータも合わせる -->
    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@stat_type='Water']/@pos">563,-77</set>
    <set xpath="/windows/window[@name='windowToolbelt']/rect/rect[@stat_type='Water']/filledsprite[@name='BarContent']/@width">562</set>
</modconfig>

以上で、Modlet に使うすべてのファイルが揃いました。

Modlet に同梱する

できあがった DLL を、ModInfo.xml と同じ階層に配置してください。ModInfo.xml の記述方法は、上記 Modlet の詳細を参考にしてください。
Config を書き換える XML ファイルも同じ階層に配置して問題ありませんが、7DTD の慣例に従うと、見やすくなるのと同時にわかりやすくなります。

ファイル配置の例:

TestProject/
├ Config/
│ └ XUi/
│   └ windows.xml
├ Harmony/
│ ├ TestProject_HarmonyInit.cs
│ └ FifteenSlotToolbelt_Inventory.cs
├ ModInfo.xml
└ TestProject.dll

既存のセーブデータの破損を避けるため、新しくセーブを作成することをおすすめします。
Mods ディレクトリに配置し、実際にゲームを起動して、ツールベルトが15個に増えているか確かめてみてください。
もしエラーが起こった場合、コンパイル時にエラーとして出てこないものは、[HarmonyPatch] 属性の指定部分です。スペルミスがないか確かめてください。
また、Alpha20.6(b9) の情報がすでに古くなっている可能性や、筆者がダウンロードした 7DTD は、もしかしたら一般的に知られている 7DTD ではない可能性が考えられます。IL をじっくり読んでエラーの改善を試してみてください。

Reference

ここではおもに https://harmony.pardeike.net/articles/patching.html の一部を邦訳したものを記載します。
気がついたら内容が増えているかもしれません。

Prefix

オリジナルメソッドが実行される前に割り込みます。以下のような場合に用いられます:

  • オリジナルメソッドの引数にアクセスしたり、変更したりする場合
  • オリジナルメソッドの戻り値を変更する場合
  • オリジナルメソッド内の処理をスキップさせ、以下で説明する Postfix にすべての処理を任せる場合
  • あらかじめ値をセットしておき、Postfix にて処理を行う場合

usage:

  • static void Prefix(ORIGINAL_ARGS...)
  • static bool Prefix(ORIGINAL_ARGS...)
  • static void Prefix(ref RETURN_TYPE __result, ORIGINAL_ARGS...)
  • static bool Prefix(ref RETURN_TYPE __result, ORIGINAL_ARGS...)
using UnityEngine;

public class OriginalCode
{
    public void Test(int counter, string name)
    {
        // ...
    }
}

[HarmonyPatch(typeof(OriginalCode))]
[HarmonyPatch("Test")]
class Patch
{
    static void Prefix(int counter, ref string name)
    {
        Debug.Log("counter = " + counter); // 値を読む
        name = "test"; // ref キーワードを設定しているため、name が変更された状態でオリジナルメソッドの処理が走る
    }
}
public class OriginalCode
{
    public string Test(int counter, string name)
    {
        // ...
    }
}

[HarmonyPatch(typeof(OriginalCode))]
[HarmonyPatch("Test")]
class Patch
{
    static bool Prefix(ref string __result, int counter, string name)
    {
        __result = "test"; // 戻り値を書き換える
        return false; // false を返すと、オリジナルメソッドの中身は実行されない
    }
}

Postfix

オリジナルメソッドが実行された後に割り込みます。以下のような場合に用いられます:

  • オリジナルメソッドの戻り値にアクセスしたり、変更したりする場合
  • オリジナルメソッドの引数にアクセスする場合
  • オリジナルメソッドが実行された後、必ず実行させたい処理がある場合
  • Prefix にて、あらかじめセットされた値を処理する場合

usage:

  • static void Postfix(ORIGINAL_ARGS...)
  • static void Postfix(ref RETURN_TYPE __result, ORIGINAL_ARGS...)
  • static RETURN_TYPE Postfix(RETURN_TYPE __result, ORIGINAL_ARGS...)

RETURN_TYPE に ref キーワードが使えない場合で、戻り値を変更したい場合、3つ目のメソッド シグネチャを使用します。

public class OriginalCode
{
    public string Test()
    {
        return this.name;
    }
}

[HarmonyPatch(typeof(OriginalCode))]
[HarmonyPatch("Test")]
class Patch
{
    static void Postfix(ref string __result)
    {
        if (__result == "test") {
            __result = "modified";
        }
    }
}
public class OriginalCode
{
    public IEnumerable<int> GetNumbers()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
}

[HarmonyPatch(typeof(OriginalCode))]
[HarmonyPatch("GetNumbers")]
class Patch
{
    static IEnumerable<int> Postfix(IEnumerable<int> __result)
    {
        yield return 0;
        foreach (var value in __result)
            if (value > 1)
                yield return value * 5;
        yield return 60;
    }
    // 戻り値は [ 1, 2, 3 ] から [ 0, 10, 15, 60 ] に変更される
}
using System.Diagnostics;
using UnityEngine;

public class OriginalCode
{
    public string Test(int counter, string name)
    {
        // ...
    }
}

[HarmonyPatch(typeof(OriginalCode))]
[HarmonyPatch("Test")]
class Patch
{
    static void Prefix(out Stopwatch __state)
    {
        __state = new Stopwatch(); // 独自の値を設定
        __state.Start();
    }

    static void Postfix(Stopwatch __state)
    {
        __state.Stop();
        Debug.Log(__state.Elapsed.ToString()); // 実行時間をログに表示
    }
}

Asset

余談

Modletとは、ゲームに変更を加えるMODと、小さい○○という意味をもつ接尾辞-letを合わせた造語です。
必要最小限の変更で済み、同じXMLを異なるMOD同士が同時に書き換えることができることから、Forumメンバーによってこのような呼称が広まりました。

リンク

https://7daystodie.com/forums/showthread.php?93816-XPath-Modding-Explanation-Thread
https://7daystodie.com/forums/showthread.php?99279-XPath-Error-Checking
https://7daystodie.com/forums/showthread.php?97419-Vanilla-Asset-Bundle-Loading-Hooks
http://www.edankert.com/xpathfunctions.html