Fabric Mixin
Mixin은 로드 시점에 마인크래프트의 바이트코드를 패치합니다. 강력하지만 깨지기 쉽습니다. 모든 Mixin은 특정 메서드와 특정 형태가 다음 버전에도 여전히 존재하고, 다른 모드가 비호환적으로 변경하지 않을 것이라는 약속입니다. 가능한 한 적게, 가능한 한 좁게 작성하세요.
결정 순서 - Mixin을 작성하기 전에 시도할 것들
- Fabric API 이벤트 또는 레지스트리. Fabric API를 먼저 검색하세요: 생명주기 이벤트(
ServerLifecycleEvents,ClientTickEvents), 플레이어 이벤트(UseBlockCallback,AttackEntityCallback,PlayerBlockBreakEvents), HUD(HudElementRegistry), 화면(ScreenEvents), 전리품(LootTableEvents), 명령어(CommandRegistrationCallback), 네트워킹, 아이템 툴팁, 리소스 리로드 리스너 등 더 많은 것들이 있습니다. https://github.com/FabricMC/fabric-api (각 모듈의api패키지)와 Fabric 문서를 참조하세요. 이벤트는 버전 간에 안정적이며 다른 모드와 자유롭게 구성됩니다. - 바닐라 확장 포인트. 데이터 기반 콘텐츠(태그, 전리품 테이블, 레시피, 데이터 컴포넌트, 레지스트리)는 종종 코드 패치가 전혀 필요하지 않습니다.
- Accessor / Invoker Mixin - private 필드를 읽거나/쓰거나 private 메서드를 호출해야 할 때만 사용합니다.
- 클래스 트위커 (access widener) - final 클래스를 서브클래싱하거나, final 메서드를 오버라이드하거나, private/package-private 클래스를 참조해야 할 때 사용합니다.
- 인젝터 Mixin - 이제서야 사용합니다.
@Redirect/@Overwrite보다 MixinExtras 인젝터를 선호하세요.
Mixin 위에 (주석으로) 1-4단계가 왜 작동하지 않았는지 기록하세요. 검토자가 물어볼 것입니다.
인젝터를 작성하기 전에: 대상 확인
이름과 시그니처는 마인크래프트 버전마다 변경됩니다. 절대 기억에 의존해 대상을 작성하지 마세요.
./gradlew genSources # 그런 다음 IDE에서 클래스를 열거나 생성된 소스를 검색하세요.
- 26.1+ (난독화 해제): 소스에 나온 그대로 Mojang 이름을 대상으로 지정합니다.
- 26.1 미만: 프로젝트의 매핑(Yarn 또는 Mojang) 이름을 대상으로 지정합니다. Loom의 Mixin 어노테이션 프로세서는 빌드 시 이를 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은 아무것도 매치하지 않는 Mixin이 조용히 아무 일도 하지 않는 대신 크게 실패하도록 만듭니다. 유지하세요.- 클라이언트 전용 대상(
net.minecraft.client.*)은 클라이언트 설정(src/client/resources에 위치,fabric.mod.json에서"environment": "client"로 선언)에 넣습니다. - Mixin 클래스는 일반 코드에서 절대 참조되지 않습니다(accessor 인터페이스는 캐스트를 통해 예외).
- 추가하는 모든 멤버 앞에 모드 ID 접두사를 붙여 다른 모드와의 충돌을 방지하세요:
examplemod$onTick,@Unique private int examplemod$counter;. - 추가하는 헬퍼 필드/메서드에는
@Unique를 표시하세요. - 핸들러 메서드는
private입니다(대상이 non-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에서 취소하면 바닐라 메서드 와 당신 이후의 모든 다른 모드의 인젝션을 건너뜁니다. 이는@Inject가 할 수 있는 가장 호환성이 낮은 일입니다. 값을 수정하는 것을 선호하세요. - 대상의 매개변수를 읽으려면 핸들러 시그니처에
CallbackInfo앞에 순서대로 추가하세요:(float amount, CallbackInfo ci). - 일반적인
@At값:HEAD,RETURN(모든 반환),TAIL(마지막 반환만),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;
}
여러 모드가 동일한 표현식에 @ModifyExpressionValue를 연결할 수 있습니다. 각 모드는 이전 모드의 결과를 받습니다. 동일한 호출에 @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); // 체인 유지
}
- 매개변수: (non-static 호출의 경우) 리시버, 그 다음 호출의 인수, 그 다음
Operation<R>(R은 래핑된 반환 타입,void의 경우Void). - 항상
original.call(...)을 호출하세요. 호출을 건너뛰는 것이 기능이 아닌 이상. 호출하지 않으면 동일한 호출에 대한 다른 모든 모드의 래퍼가 조용히 비활성화됩니다.
@WrapWithCondition - 조건부로 void 호출 건너뛰기
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, 호출 인수... */) {
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에서 변경, 다른 모드와 연결됨), @ModifyReceiver, @WrapMethod (전체 메서드 감싸기), @Share (동일한 메서드의 두 인젝터 간에 값 전달), @Cancellable (@WrapOperation 내부에서 취소).
@Redirect 및 @ModifyVariable - 다른 방법이 없을 때만
@Redirect는 호출을 대체합니다. 하나의 모드만 특정 호출을 리디렉션할 수 있습니다. 두 번째 모드의@Redirect(또는 일부 순서에서 리디렉션된 호출 위에 적용된@WrapOperation)는 하드 크래시와 함께 실패합니다.@WrapOperation을 대신 사용하세요.@ModifyVariable은 타입과 ordinal로 지역 변수를 대상으로 하며, 이는 버전 간에 변경됩니다. 값을 생성하는 표현식에@ModifyExpressionValue를 사용하거나@Local로 읽으세요.@Overwrite는 전체 메서드를 대체합니다: 이 메서드를 건드리는 다른 모든 모드와 호환되지 않습니다. 공개 모드에서는 사용하지 마세요.
Accessor 및 Invoker
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();
- 인스턴스 accessor 메서드는 모드 ID 접두사를 가져옵니다. 정적 메서드(
static T getX() { throw new AssertionError(); })는 접두사가 필요하지 않습니다. - setter에
@Mutable을 사용하면 대상 필드에서final을 제거합니다. - 생성자 invoker:
@Invoker("<init>") static Foo examplemod$new(...). - Accessor는 다른 모드의 클래스에서도 작동합니다. 클래스 트위커는 바닐라에서만 작동합니다.
클래스 트위커 (이전의 access widener)
final 클래스를 서브클래싱하거나, final 메서드를 오버라이드하거나, public이 아닌 클래스를 참조해야 할 때 필요합니다. Loader 0.18+ 및 Loom 1.12+가 필요합니다. 이전 설정은 accessWidener v2 named 헤더와 함께 "access widener"라는 이름으로 동일한 구문을 사용합니다.
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를 실행하세요. 유효하지 않은 항목은 조용히 무시됩니다. 모드가 의존하는 다른 모드가 변경 사항을 볼 수 있어야 하는 경우에만 transitive- 지시문을 사용하세요. 헤더 세부 정보, v2 기능(인터페이스 인젝션, 열거형 확장): https://docs.fabricmc.net/develop/class-tweakers/
크로스 모드 호환성 체크리스트
- 가능한 가장 좁은 지점에 인젝션하세요. 널리 사용되는 메서드(
tick,render,hurt,travel)의HEAD에서 취소하지 마세요. 확실하지 않다면. - 대체하는 인젝터(
@Redirect,@Overwrite)보다 연결되는 인젝터(@ModifyExpressionValue,@ModifyReturnValue,@WrapOperation)를 선호하세요. - 추가된 모든 상태에
@Unique+ 모드 ID 접두사를 사용하세요. @At("RETURN")이 작동할 때ordinal과@At("TAIL")을 피하세요.- 알려진 충돌을 해결할 때만
@Mixin(value = X.class, priority = ...)를 사용하고 문서화하세요. - 다른 모드를 대상으로 하는 선택적 Mixin: 별도의 설정에 넣고 Mixin 플러그인(
IMixinConfigPlugin#shouldApplyMixin이FabricLoader.getInstance().isModLoaded("othermod")를 확인)으로 게이트를 걸거나 해당 인젝터에"require": 0을 사용하세요. - 알려진 무거운 패처(Sodium, Iris, Lithium, ImmediatelyFast)는 렌더러와 게임 로직의 큰 부분을 대체합니다. 렌더링이나 엔티티 틱에 인젝션하는 경우 함께 테스트하세요.
Mixin 디버깅
개발 실행 구성에 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/...에는 변환된 클래스가 포함되어 있습니다. 디컴파일하여 인젝터가 정확히 무엇을 생성했는지와 다른 모드의 인젝터가 어떤 순서로 실행되었는지 확인하세요.-Dmixin.checks.interfaces=true는 Mixin이 인터페이스 메서드를 구현하는지 확인합니다.- 디코딩 오류(대상을 찾을 수 없음,
InvalidInjectionException, 충돌하는 리디렉션):fabric-crash-debugging스킬을 참조하세요.
일반적인 실수
- Fabric API에 이미 이벤트가 있는 것(HUD, 틱, 블록 사용, 툴팁, 명령어, 전리품 테이블)에 Mixin을 작성하는 경우.
- 메서드 이름이나 디스크립터를 기억이나 다른 버전/매핑 세트에서 추측하는 경우. 항상 생성된 소스를 먼저 확인하세요.
- Mojang 이름을 사용하는 프로젝트에서 Yarn 이름(
MinecraftClient,PlayerEntity)을 사용하거나 그 반대의 경우. @WrapOperation/@ModifyExpressionValue가 가능한데@Redirect를 사용하는 경우. 공개 모드에서@Overwrite를 사용하는 경우.cancellable = true+ci.cancel()을HEAD에서 기본적인 "변경" 방식으로 사용하는 경우.@WrapOperation에서original.call(...)을 잊어버리는 경우.CallbackInfoReturnable제네릭이 잘못된 경우(래핑 대신 기본 타입 사용), 또는 필요할 때 대상의 매개변수를ci앞에 추가하지 않는 경우.- Mixin 클래스가 설정에 나열되지 않았거나, 잘못된 섹션(
mixins대신client)에 나열되었거나, 설정의package와 다른 패키지에 있는 경우. - 공통 설정에 클라이언트 대상을 넣는 경우: 전용 서버가 시작 시 크래시납니다.
- 일반 코드에서 Mixin 클래스를 직접 참조하는 경우(
new LivingEntityMixin()), 또는 Mixin 패키지에 Mixin이 아닌 헬퍼 클래스를 넣는 경우. @Unique가 아니고 접두사가 없는 필드가 다른 모드의 Mixin과 충돌하는 경우.- 실패하는 Mixin을 "고치기" 위해
defaultRequire를 끄는 경우. 이는 버그를 숨기고 조용히 아무것도 하지 않는 모드를 출시하는 것입니다. - 클래스 트위커가
build.gradle/fabric.mod.json중 하나에만 선언되었거나 검증되지 않은 경우.