昨日、自作のmodについてのうすい記事を書いた。
naari.hatenablog.com
このmodを公開するに至って大変だったことをつらつらとかきます
fabricのMixinやInjectsの概念についてのドキュメントが少なすぎる
fabricにはMixinや、Injectsという概念がある。これは元々のMinecraft のコードに自分の好きな挙動を差し込むことができる。
たとえば以下のようなコードがあった場合、
class Example {
void blah() {
System.out.println("hogefuga" );
}
}
以下のようなコードを書くことができる。
@Mixin (Example.class )
abstract class ExampleMixin {
@Inject (at = @At ("HEAD" ), method = "blah" )
void yo(ci CallbackInformation) {
System.out.println("foobarbar" );
}
}
この状態で Example#blah()
を実行すると foobarbar
と hogefuga
がプリントされる。
また、以下のように処理のキャンセルを行うこともできる。
@Mixin (Example.class )
abstract class ExampleMixin {
@Inject (at = @At ("HEAD" ), method = "blah" )
void yo(ci CallbackInformation) {
System.out.println("foobarbar" );
ci.cancel();
}
}
こうすることで、本来プリントされるはずの hogefuga
はキャンセルされる。これは純粋にもとのメソッドの内容をすべてオーバライドするために使うこともできる上に、特定の条件下でのみバイパスする、といった処理も実現できる。
メソッドが呼ばれる際の引数を参照することもできるため、基本的にはこれを利用してmoddingを行うことになる。
検索して出る情報が少なすぎる
公式のドキュメントを含めて、純粋にこの@Mixinについての情報が少なすぎて大変に困っていた。最終的にはこの@Mixinを使っているmodのソースコード を参考にせざるを得なかった。
tutorial:mixin_injects [Fabric Wiki]
fabric-carpet/ServerPlayNetworkHandler_scarpetEventsMixin.java at af368a159d2c3c287acbd22d9a9c0f0094519c56 · gnembon/fabric-carpet · GitHub
結局実際の実装が一番参考になるのがドキュメントとしておかしい気がしていて、そのうち大きく手を入れることができそうに思った。日本語の情報もないし、もしそれが提供できたら喜ぶ人が結構いるのではないかと思う。
村人のgetOffers()を叩くだけだとダメ
元々のプランだと村人にフォーカスをあてた時にそのEntityの getOffers()
を叩いて左上に表示しようかな~という気持ちだったが、そもそもこれが不充分だった。
用語: MerchantEntity について
村人などの「右クリック→画面が開いて取引開始」が実装されてるEntityはすべて MerchantEntity
を継承している。
通信の流れ
プレイヤーが MerchantEntity
に右クリックをした時、サーバー側に「このプレイヤーがこのEntityにinteractを行った」という趣旨のパケットを送信する。
サーバー側はこのパケットを受信した後、よしなにチェックを行い、最終的にはプレイヤーに「取引のウィンドウを開く」と「取引内容」のパケットを送信する。
この取引内容のパケットを元に MerchantEntity
に最新の取引内容をセットするため、このパケットを掴まないとプレイヤーが望んだ情報を提供できない。
つまり、右クリックをしないと取引内容を取得することができない ということになる。
擬似的に右クリックする
これに対してどのような方針で進めるのが最適なのかはわからないが、今回は以下のような方針を取った。
フォーカスが MerchantEntity
に当たった場合、サーバーにinteractを行うパケットを送信する
サーバー側から「取引のウィンドウを開く」のパケットが届く
サーバー側から「取引内容」のパケットが届く
もし MerchantEntity
を直接右クリックしていた場合は以上の処理をスキップする
正直筋が良い方法はわからないままだが、上述したMixinとInjectorsを使用してパケットハンドラーに直接処理を挟むことにした。
offers-hud/ReceiveTradeOfferPacket.java at main · naari3/offers-hud · GitHub
@Mixin (ClientPlayNetworkHandler.class )
abstract class ReceiveTradeOfferPacket {
@Inject (at = @At ("HEAD" ), method = "onSetTradeOffers" , cancellable = true )
public void onSetTradeOffers(SetTradeOffersS2CPacket packet, CallbackInfo ci) {
MerchantInfo.getInfo().setOffers(packet.getOffers());
if (!OffersHUD.getOpenWindow()) {
ci.cancel();
}
}
@Inject (at = @At ("HEAD" ), method = "onOpenScreen" , cancellable = true )
public void onOpenScreen(OpenScreenS2CPacket packet, CallbackInfo ci) {
var type = packet.getScreenHandlerType();
if (!OffersHUD.getOpenWindow() && type == ScreenHandlerType.MERCHANT) {
ci.cancel();
ClientPlayNetworking.getSender()
.sendPacket(new CloseHandledScreenC2SPacket(packet.getSyncId()));
}
}
}
このリポジトリ を作ったのはおおよそ1年前だった気がするのだが、パケットハンドラーに手を出すという発想に至れたのはひとえにMinecraft のサーバーを1から作成したことによるものに思える。
画面上になにかを描画するのが難しい
これもドキュメントについての愚痴で、チュートリアル 的なものが全くないためにどのような手段を踏むのが正攻法なのかが全く分かっていない。
幸いにも、今回の要件は「MerchantEntityの取引内容を描画する」という、既に同じような実装が存在するものだったのでこれを流用することでなんとかなった。
具体的には、 itemRenderer#renderInGui()
を使うことでアイテムのテクスチャつきで描画をすることができる。テキストについても同じように textRenderer
とその便利なメソッドが存在する。
もしもうちょっと踏み込んだレンダリング が必要になった場合、どうやって書けばよいのだろう。全くできる気がしない。
1.18になって何故か引数の順番が変わっててワロタ
Minecraft の画面上にテクスチャを描画するためには一般的に DrawableHelper.drawTexture
が使われるようだが、この引数が1.17.x と 1.18.x で変更されていた。具体的には以下のように。
void DrawableHelper.drawTexture(MatrixStack matrices, int x, int y, int z, float u, float v, int width, int height, int textureHeight, int textureWidth);
void DrawableHelper.drawTexture(MatrixStack matrices, int x, int y, int z, float u, float v, int width, int height, int textureWidth, int textureHeight);
こんなのに一発で気づくわけもなく、「なんで1.18では正しく描画されなくなっちゃったんだろう」とかなり困っていたが、fabricのためのDiscordサーバーで尋ねてみたところ、すぐにこの答えを返してくれた。本当にありがとうございました。
こちらから参加できるみたいです discuss | Fabric
modをCurseForgeにpublishする方法
今回、わりと使えるmodが作れたのではないかという自負があり、伴って一般的なmodの配布場所である CurseForge にも配布することにした。
CurseForge上にプロジェクトを作成する必要があった
ただ、そもそも会員登録すらしていなかったので、会員登録をし、さてどうやってmodを公開するのだろう、と3時間くらいサイト上を彷徨っていた。
authors のトップ画面にアクセスする
Start Your Project をクリックする
あとは流れで
ずっと Dashborad の上を見ていたが、このような新規作成系のボタンがひとつも存在しなかった。こういうのって普通ダッシュ ボードの上にあるんじゃないの?かなり難しい。
GitHub Actions を利用して公開する
その後、実際に実装した後はmodをプロジェクト上に公開する必要があるが、これがまた難しかった。
.jarファイルを生成した後にプロジェクト上に手動でアップロードする、ということもできるが、昨今のCI/CDの時代にそんなことをしていたら時間がもったいない。自動化するべきである。
これも人様の実装を参考にする
実際に、先程も参考として取り上げたgnembonのcarpetは GitHub Actions を使用してCurseForgeにmodの公開をしている。releaseを打つ度に発火されるようだった。
fabric-carpet/publish-release.yml at master · gnembon/fabric-carpet · GitHub
今回はこれをコピペし、自分のプロジェクトに向けてもう少しだけ調整したものを用意した。
具体的な実装はこれ → offers-hud/publish-release.yml at main · naari3/offers-hud · GitHub
コピペしました!だけだと味気がないので、もうちょっとだけ踏み込んでみる。↓
itsmeow/curseforge-upload を使用してCurseForgeに生成物をアップロードする
CurseForgeにファイルをアップロードするには、itsmeow/curseforge-upload
という公開されている action を使用する。
github.com
自分の使い方は以下の通りで、action にトーク ンやプロジェクトの情報を渡したらあとはよしなに動いてくれる。チェンジログ をreleaseのbodyに書けるのがかなり便利だと思った。
- name : Upload to Curseforge
uses : itsmeow/curseforge-upload@v3
with :
token : ${{ secrets.CF_API_TOKEN }}
project_id : 566138
game_endpoint : minecraft
file_path : build/libs/${{ steps.findjar.outputs.jarname }}
changelog_type : markdown
changelog : ${{ github.event.release.body }}
display_name : OffersHUD v${{ needs.Get-Properties.outputs.mod-version }} for ${{ steps.getbranchinfo.outputs.version }}
game_versions : 7499 ,4458 ,${{ steps.getbranchinfo.outputs.curse-versions }}
release_type : ${{ needs.Get-Properties.outputs.release-type }}
game_version がすごく難しい
Game Versionというのは、CurseForge内で「そのプロジェクトがどのゲームのどのバージョンに対応したものか?」というのを表すためのもので、例えばこのプロジェクトだと fabric 、Java 8*1 、1.18 、1.18.1 に対応したmodなのでそれぞれを指定する必要がある。
fabric も Java 8 もゲームじゃなくない?と思ったけど、もうそうやって管理されているから仕方がないらしい。
これが結構曲者で、たとえば純粋に 1.18.1
という文字列を指定すると怒られる場合がある。例えば以下のように。
Publish Release · naari3/offers-hud@d02f693 · GitHub
{ "errorCode ":1009 ,"errorMessage ":"Invalid game version ID: 8897 belongs to an invalid dependency. "}
これは 1.18.1
という文字列に対応した Game Version が複数あることが原因で、この場合は全然知らない名前のゲームの1.18.1を指定してしまっていたらしい。
なので、正しくMinecraft の1.18.1に対応したGame Versionのidを発見し、そのidを直接指定する必要があった。
release-extra-curse-version = 8857 ,8830
さてこのidの対応表だが、どこかにドキュメントとしてまとまっているわけではなく、以下に存在するjson から妥当なものを探し出す必要がある。
[https://minecraft.curseforge.com/api/game/versions?token= [API _TOKEN]]
このjson 内で 1.18.1
を検索すると次の2つが発見できる。
{
"id ": 8897 ,
"gameVersionTypeID ": 1 ,
"name ": "1.18.1 ",
"slug ": "1-18-1 "
} ,
// snip
{
"id ": 8857 ,
"gameVersionTypeID ": 73250 ,
"name ": "1.18.1 ",
"slug ": "1-18-1 "
} ,
どちらがMinecraft のものか全くわからん…………………
解決方法としては、CurseForgeのmod検索ページの絞り込みで1.18.1を指定すると次のようなURLに遷移する。
https://www.curseforge.com/minecraft/modpacks?filter-game-version=2020709689%3A8857&filter-sort=4
このうち filter-game-version
を確認することで解決できた。今回の場合は 8857
が正しかった。
また、これによって gameVersionTypeID: 73250
が Minecraft 1.18系 を指すことが分かったため、このidで検索すると Minecraft 1.18 や snapshot に相当する Game Version も発見できる。
{
"id ": 8633 ,
"gameVersionTypeID ": 73250 ,
"name ": "1.18-Snapshot ",
"slug ": "1-18-snapshot "
} ,
// snip
{
"id ": 8830 ,
"gameVersionTypeID ": 73250 ,
"name ": "1.18 ",
"slug ": "1-18 "
} ,
本当は一発でこのあたりが網羅的に確認できるのが嬉しいが、どうやらそういった対応表は存在しなさそうだった…
とまあこんな感じで、人の実装を参考にしまくった結果このmodが完成した。
その中で、「そういえばこの処理ってあのmodで実現されていたな、どうやっているんだろう」と思い出し、そのmodのページにたどり着いたりするが、感覚でいうと半分くらいの確率でソースコード が公開されていない。
昨今の問題もあるのでこのあたりについてあまり強いことは言えないが、mod制作の参考に出来ないのはシンプルに悲しかった。もし公開状態であればPRを投げることも出来そうなのに。
おおよその場合において難読化が行われていないので、一応は JD-GUI などを使用することで確認を行える。
ただ、以下のようにMinecraft のdeobfuscatedなソースコード とのmappingは切られた状態でビルドされるため、具体的にどのクラスのどのメソッドと対応しているか?はfabricで使用されているMinecraft のdeobfuscatorである yarn と照らし合わせる必要がありそう。
むずい
Configを実装しなければならない
現状、機能のオン/オフすら出来ない。これだと他modとの競合があり、場合によってはまともに使うことができないだろう。
これについてもConfigを実現するための参考実装を探す必要があるが、どれを参考にすればよいのだろうか…
このあたりについてもチュートリアル は存在しないため、情報収集からデファクト を感じ取る必要があり、結構大変そうに思う。
ここから追記
ここ→ documentation:libraries [Fabric Wiki] に使えるライブラリ一覧がまとまっていて、それぞれ検索して良さそうなものを見繕った。
結果として、ClothConfig を使うのが良さそうに思った。どうやら過去デファクト になっていたAutoConfigも同梱しているらしい。
Classとしてプロパティを実装していくだけでConfigとmodmenuに対応したscreenを実装できる。
@Config (name = OffersHUD.MODID)
public class ModConfig implements ConfigData {
public boolean enabled = true ;
public boolean ignoreNoProfession = true ;
public boolean suppressVillagerHeadRolling = false ;
}
これとlangファイルで以下のようなスクリーンが生成される。嬉しいね
ここまで追記
まとめ
こんな感じで、全てにおいて他人の実装を参考にせざるを得ず、自分ひとりの力では実装しづらい部分が多々あることに気づけた。fabricのDiscordサーバー も存在するので、臆することなくどんどん利用していく必要があるように感じた。