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折り畳み
元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折り畳み
元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折り畳み
元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折り畳み
元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折り畳み
元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(要素の置き換え)折り畳み
元XML
<items> <item name="1"/> </items>
XML操作XML
<configs>
<set xpath="/items">
<item name="2"/>
</set>
</configs>
出力XML
<items>
<item name="2"/>
</items>
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折り畳み
元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折り畳み
元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中で使う場合は、>=と記述します <= 左が右と同じかそれより小さいか xml中で使う場合は、<=と記述します > 左が右より小さいか xml中で使う場合は、>と記述します < 左が右より小さいか xml中で使う場合は、<と記述します == 左と右が同じか != 左と右が異なるか <> 左が右より大きいか小さいか xml中で使う場合は、<>と記述します
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で始まる要素を指定できます。
- position()
- 要素の番号を返します。
[position() < 3]とすれば、3番目より後の要素を指定でき、
[position() > 3]とすれば、3番目より前の要素を指定できます。
(<は<の、>は>の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