Fabricミキシン
ミキシンは、ロード時にMinecraftのバイトコードをパッチします。強力ですが繊細です。すべてのミキシンは、特定のシグネチャを持つ特定のメソッドが次のバージョンでも存在し、他のMODが互換性を壊すように変更しないことを約束します。可能な限り少なく、可能な限り狭く記述してください。
決定順序 – ミキシンを書く前にこれらを試す
- Fabric APIのイベントまたはレジストリ。 まずFabric APIを調べてください。ライフサイクルイベント (
ServerLifecycleEvents,ClientTickEvents)、プレイヤーイベント (UseBlockCallback,AttackEntityCallback,PlayerBlockBreakEvents)、HUD (HudElementRegistry)、画面 (ScreenEvents)、 loot (LootTableEvents)、コマンド (CommandRegistrationCallback)、ネットワーキング、アイテムツールチップ、リソースリロードリスナーなどがあります。 https://github.com/FabricMC/fabric-api (各モジュールのapiパッケージ) とFabricドキュメントを参照してください。イベントはバージョン間で安定しており、他のMODと無料で合成できます。 - バニラの拡張ポイント。 データ駆動型のコンテンツ (タグ、ルートテーブル、レシピ、データコンポーネント、レジストリ) は、多くの場合、コードパッチがまったく必要ありません。
- アクセサー/インボーカーミキシン。 プライベートフィールドの読み取り/書き込みや、プライベートメソッドの呼び出しのみが必要な場合。
- クラスツイーカー (アクセスワイドナー)。 finalクラスをサブクラス化したり、finalメソッドをオーバーライドしたり、非パブリッククラスを参照したりする必要がある場合。
- インジェクターミキシン。 ここで初めて使います。
@Redirect/@Overwriteよりも MixinExtras インジェクターを優先してください。
ステップ1〜4が機能しなかった理由を (ミキシンの上にあるコメントに) 記述してください。レビュアーが尋ねます。
インジェクターを書く前に: ターゲットを確認する
名前とシグネチャはMinecraftのバージョンごとに変わります。ターゲットを記憶から書かないでください。
./gradlew genSources # その後、IDEでクラスを開くか、生成されたソースを検索します
- 26.1以降 (難読化解除): Mojang名をソースに表示されている通りに正確にターゲットにします。
- 26.1より前: プロジェクトのマッピング (YarnまたはMojang) の名前をターゲットにします。Loomのミキシンアノテーションプロセッサはビルド時にそれらをintermediaryに再マッピングします。不一致があると、コンパイル時に「Cannot remap」警告として表示されます。その警告をエラーとして扱ってください。
- メソッド記述子は、バイトコードビューまたはツール (MCDev IntelliJプラグイン、mcsrc.dev) からコピーしてください。手動で入力しないでください。記述子リファレンス: https://docs.fabricmc.net/develop/mixins/bytecode
Mixin設定と規約
{
"required": true,
"package": "com.example.examplemod.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
"client": [],
"server": [],
"injectors": { "defaultRequire": 1 },
"overwrites": { "requireAnnotations": true }
}
"defaultRequire": 1は、何もマッチしないミキシンを、何もせずに静かに動作する代わりに大きく失敗させます。そのままにしておいてください。- クライアント専用ターゲット (
net.minecraft.client.*) は、クライアント設定 (src/client/resourcesにあり、fabric.mod.jsonで"environment": "client"と宣言されています) に配置します。 - ミキシンクラスは、通常のコードから参照されることは決してありません (アクセサーインターフェースは、キャストを介して除く)。
- 追加するすべてのメンバーには、他のMODとの衝突を避けるために、mod-idの接頭辞を付けてください:
examplemod$onTick,@Unique private int examplemod$counter;。 - 追加するヘルパーフィールド/メソッドには
@Uniqueをマークします。 - ハンドラーメソッドは
privateです (ターゲットが非staticの場合は非static)。
以下の例のターゲットはMojang名を使用し、各インジェクターの形状を示しています。メソッド名と記述子はバージョン間で変化します – 再利用する前に、生成されたソースでそれぞれを再確認してください。
@Inject – ある時点でコードを実行し、必要に応じてキャンセルする
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
@Inject(method = "jumpFromGround", at = @At("HEAD"), cancellable = true)
private void examplemod$cancelJump(CallbackInfo ci) {
if (ExampleConfig.get().disableJump) {
ci.cancel();
}
}
// getJumpPower はオーバーロードされています (getJumpPower() と getJumpPower(float)): 記述子を指定します。
@Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(cir.getReturnValueF() * 1.5f);
}
}
voidターゲットの場合はCallbackInfo、それ以外の場合はCallbackInfoReturnable<T>(ボックス化された型) を使用します。- 実際にキャンセルする場合にのみ
cancellable = trueを宣言してください。HEADでキャンセルすると、バニラのメソッドと、その後にある他のすべてのMODのインジェクションがスキップされます – これは@Injectができる中で最も互換性の低いことです。値を変更することを優先してください。 - ターゲットのパラメータを読み取るには、
CallbackInfoの前に、ハンドラーのシグネチャに順番に追加します:(float amount, CallbackInfo ci)。 - 一般的な
@Atの値:HEAD,RETURN(すべてのreturn),TAIL(最後のreturnのみ),INVOKE(呼び出しの前。後に続けるにはshift = At.Shift.AFTERを追加),FIELD,NEW。 ordinalは必要な場合にのみ使用してください。Mojangが別のマッチング呼び出しを追加するとすぐに壊れます。
MixinExtras – @Redirect とほとんどの @ModifyVariable の代わりにこれらを使用する
MixinExtrasは Fabric Loader 0.15+ に同梱されているため、追加の依存関係は必要ありません。インポートは com.llamalad7.mixinextras の下にあります。ドキュメント: https://github.com/LlamaLad7/MixinExtras/wiki
@ModifyExpressionValue – 呼び出し/フィールド読み取りが返すものを変更する
@ModifyExpressionValue(
method = "travelInWater",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getSpeed()F")
)
private float examplemod$fasterInWater(float original) {
return original * 1.2f;
}
複数のMODが同じ式に @ModifyExpressionValue をチェーンできます。各MODは前のMODの結果を受け取ります。同じ呼び出しに対する @Redirect は競合してクラッシュします。
@WrapOperation – 呼び出しをラップし、元の呼び出しを実行するかどうかを選択する
@WrapOperation(
method = "hurtServer",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;actuallyHurt(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;F)V")
)
private void examplemod$logDamage(LivingEntity self, ServerLevel level, DamageSource source,
float amount, Operation<Void> original) {
ExampleMod.LOGGER.debug("{} took {} damage", self, amount);
original.call(self, level, source, amount); // チェーンを維持する
}
- パラメータ: レシーバー (非static呼び出しの場合)、次に呼び出しの引数、次に
Operation<R>(Rはボックス化された戻り値の型。voidの場合はVoid)。 - 呼び出しをスキップすることが機能でない限り、常に
original.call(...)を呼び出してください。 呼び出さないと、同じ呼び出しに対する他のすべてのMODのラッパーが静かに無効になります。
@WrapWithCondition – void呼び出しを条件付きでスキップする
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, call args... */) {
return !ExampleConfig.get().hideThing; // false = 呼び出しをスキップ
}
@Local – @ModifyVariable を使わずにローカル変数を読み取る
@Inject(method = "tick", at = @At(value = "INVOKE", target = "..."))
private void examplemod$readLocal(CallbackInfo ci, @Local(name = "pos") BlockPos pos) { ... }
26.1以降では、ゲームにパラメータ名とローカル変数名が含まれているため、@Local(name = ...) はそこで信頼できます。26.1より前 (難読化されたjarにローカル変数名がない) では、タイプごとに @Local(ordinal = n) を優先し、mixin.debug.export で確認してください。
その他の便利な MixinExtras ツール
@ModifyReturnValue (メソッドの戻り値を RETURN で変更、他のMODとチェーン)、@ModifyReceiver、@WrapMethod (メソッド全体をラップ)、@Share (同じメソッド内の2つのインジェクター間で値を渡す)、@Cancellable (@WrapOperation 内からキャンセル)。
@Redirect と @ModifyVariable – 他に適切なものがない場合のみ
@Redirectは呼び出しを置き換えます。1つのMODだけが特定の呼び出しをリダイレクトできます。2つ目のMODの@Redirect(または、リダイレクトされた呼び出しの上に適用された@WrapOperationが特定の順序で) は、ハードクラッシュで失敗します。代わりに@WrapOperationを使用してください。@ModifyVariableはローカル変数をタイプとordinalでターゲットにしますが、これはバージョン間で変わります。値を生成する式に対して@ModifyExpressionValueを使用するか、読み取るために@Localを使用してください。@Overwriteはメソッド全体を置き換えます。それに触れる他のすべてのMODと互換性がありません。公開MODでは使用しないでください。
アクセサーとインボーカー
package com.example.examplemod.mixin.accessor;
@Mixin(LivingEntity.class)
public interface LivingEntityAccessor {
@Accessor("noJumpDelay")
int examplemod$getNoJumpDelay();
@Accessor("noJumpDelay")
void examplemod$setNoJumpDelay(int value);
@Invoker("getJumpPower")
float examplemod$invokeGetJumpPower();
}
// 使用法
int delay = ((LivingEntityAccessor) entity).examplemod$getNoJumpDelay();
- インスタンスアクセサーメソッドはmod-idの接頭辞を取得します。staticメソッド (
static T getX() { throw new AssertionError(); }) はそれを必要としません。 - セッターの
@Mutableは、ターゲットフィールドからfinalを削除します。 - コンストラクターインボーカー:
@Invoker("<init>") static Foo examplemod$new(...)。 - アクセサーは他のMODのクラスでも機能します。クラスツイーカーはバニラでのみ機能します。
クラスツイーカー (旧アクセスワイドナー)
final クラスをサブクラス化したり、final メソッドをオーバーライドしたり、非パブリッククラスを参照したりするために必要です。Loader 0.18+ と Loom 1.12+ が必要です。古いセットアップでは、accessWidener v2 named ヘッダーを持つ「アクセスワイドナー」という名前で同じ構文を使用します。
src/main/resources/examplemod.classtweaker:
classTweaker v1 official
accessible class net/minecraft/world/inventory/MenuType$MenuSupplier
accessible method net/minecraft/network/chat/TextColor formatValue ()Ljava/lang/String;
accessible field net/minecraft/world/damagesource/DamageSources damageTypes Lnet/minecraft/core/Registry;
両方の場所で配線し、Gradleをリフレッシュして genSources を再実行します:
loom { accessWidenerPath = file("src/main/resources/examplemod.classtweaker") }
"accessWidener": "examplemod.classtweaker"
./gradlew validateAccessWidener を実行します – 無効なエントリは、他に静かに無視されます。あなたのMODに依存するMODが変更を認識する必要がある場合にのみ transitive- ディレクティブを使用してください。ヘッダーの詳細、v2機能 (インターフェースインジェクション、Enum拡張): https://docs.fabricmc.net/develop/class-tweakers/
クロスMOD互換性チェックリスト
- 機能する最も狭いポイントでインジェクトしてください。確信がない限り、広く使用されているメソッド (
tick,render,hurt,travel) のHEADでキャンセルしないでください。 - 置き換えるインジェクター (
@Redirect,@Overwrite) よりもチェーンするインジェクター (@ModifyExpressionValue,@ModifyReturnValue,@WrapOperation) を優先してください。 - 追加するすべての状態に
@Unique+ mod-id プレフィックスを使用してください。 @At("RETURN")が機能する場合は、ordinalと@At("TAIL")を避けてください。@Mixin(value = X.class, priority = ...)は、既知の競合を解決するためにのみ使用し、文書化してください。- 別のMODをターゲットにするオプションのミキシン: それらを別の設定に入れ、ミキシンプラグイン (
IMixinConfigPlugin#shouldApplyMixinがFabricLoader.getInstance().isModLoaded("othermod")をチェック) でゲートするか、そのインジェクターで"require": 0を使用してください。 - 既知のヘビーパッチャー (Sodium, Iris, Lithium, ImmediatelyFast) は、レンダラーとゲームロジックの大部分を置き換えます。レンダリングやエンティティのティックにインジェクトする場合は、それらでテストしてください。
ミキシンのデバッグ
開発実行設定にJVM引数を追加します:
loom {
runs {
configureEach {
vmArg "-Dmixin.debug.export=true" // 変換されたクラスを run/.mixin.out/ に書き込む
vmArg "-Dmixin.debug.verbose=true"
vmArg "-Dmixin.dumpTargetOnFailure=true"
}
}
}
run/.mixin.out/class/...には変換されたクラスが含まれています – それを逆コンパイルして、インジェクターが何を生成したか、他のMODのインジェクターがどの順序で実行されたかを正確に確認できます。-Dmixin.checks.interfaces=trueは、ミキシンがインターフェースメソッドを実装していることを検証します。- デコードエラー (ターゲットが見つからない、
InvalidInjectionException、競合するリダイレクト):fabric-crash-debuggingスキルを参照してください。
よくある間違い
- Fabric APIにすでにイベントがあるものに対してミキシンを書く (HUD、tick、ブロック使用、ツールチップ、コマンド、ルートテーブル)。
- メソッド名や記述子を記憶や別のバージョン/マッピングセットから推測する。最初に生成されたソースを常に確認してください。
- Mojang名のプロジェクトでYarn名 (
MinecraftClient,PlayerEntity) を使用する、またはその逆。 @WrapOperation/@ModifyExpressionValueで十分なところで@Redirectを使用する。公開MODで@Overwriteを使用する。- 動作を「変更する」デフォルトの方法として
cancellable = true+ci.cancel()をHEADで使用する。 @WrapOperationでoriginal.call(...)を忘れる。CallbackInfoReturnableのジェネリクスが間違っている (プリミティブの代わりにボックス化)、または必要なときにターゲットのパラメータをciの前に追加し忘れる。- ミキシンクラスが設定にリストされていない、間違ったセクション (
mixins対client) にリストされている、または設定のpackageとは異なるパッケージにある。 - 共通設定のクライアントターゲット: 起動時に専用サーバーがクラッシュします。
- 通常のコードからミキシンクラスを直接参照する (
new LivingEntityMixin()) 、またはミキシンパッケージ内に非ミキシンヘルパークラスを配置する。 - 別のMODのミキシンと衝突する、
@Uniqueではなく、接頭辞のないフィールド。 - 失敗しているミキシンを「修正」するために
defaultRequireをオフにする – これはバグを隠し、何もせずに静かに動作するMODを出荷します。 - クラスツイーカーが
build.gradle/fabric.mod.jsonの一方だけで宣言されている、または検証されていない。