昨日、自作のmodについてのうすい記事を書いた。
このmodを公開するに至って大変だったことをつらつらとかきます
- fabricのMixinやInjectsの概念についてのドキュメントが少なすぎる
- 村人のgetOffers()を叩くだけだとダメ
- 画面上になにかを描画するのが難しい
- modをCurseForgeにpublishする方法
- 人のmodがオープンソースじゃなくて悲しい
- Configを実装しなければならない
- まとめ
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]
結局実際の実装が一番参考になるのがドキュメントとしておかしい気がしていて、そのうち大きく手を入れることができそうに思った。日本語の情報もないし、もしそれが提供できたら喜ぶ人が結構いるのではないかと思う。
村人の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 で変更されていた。具体的には以下のように。
// 1.17 void DrawableHelper.drawTexture(MatrixStack matrices, int x, int y, int z, float u, float v, int width, int height, int textureHeight, int textureWidth); //1.18 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 を使用する。
自分の使い方は以下の通りで、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 }} #Fabric,Java 8,[version (s) for the branch] 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を直接指定する必要があった。
# これは Minecraft の 1.18.1 と 1.18 を指す 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のページにたどり着いたりするが、感覚でいうと半分くらいの確率でソースコードが公開されていない。
昨今の問題もあるのでこのあたりについてあまり強いことは言えないが、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サーバーも存在するので、臆することなくどんどん利用していく必要があるように感じた。
*1:これ違くない??直さなきゃ…