Community코딩 & 개발github.com

vvs1off/fabric-mixins

Fabric 모드에서 Mixin을 작성, 검토 또는 수정합니다. Fabric API 이벤트와 Mixin 중에서 선택하고, @Inject, @Redirect, @ModifyVariable, MixinExtras의 안전한 인젝터(@WrapOperation, @ModifyExpressionValue, @WrapWithCondition, @Local), accessor/invoker, 클래스 트위커(access widener), 크로스 모드 호환성 및 mixin.debug.export를 사용한 디버깅을 지원합니다. 바닐라 동작 변경, private 필드 읽기 또는 Mixin 적용 실패 시 사용하세요.

fabric-mixins란 무엇인가요?

이 스킬은 Fabric 모드 개발자가 Mixin을 안전하고 효과적으로 사용할 수 있도록 돕습니다. Mixin은 마인크래프트의 바이트코드를 로드 시점에 패치하는 강력하지만 깨지기 쉬운 도구입니다. 이 스킬은 Mixin을 작성하기 전에 먼저 Fabric API 이벤트, 바닐라 확장 포인트, accessor/invoker, 클래스 트위커 등 더 안정적인 대안을 고려하도록 안내합니다. 실제 Mixin 작성 시에는 @Inject, @Redirect, @ModifyVariable과 함께 MixinExtras 라이브러리(@WrapOperation, @ModifyExpressionValue, @WrapWithCondition, @Local)를 사용하도록 권장하며, 특히 @Redirect보다 MixinExtras 인젝터를 선호합니다. 또한 크로스 모드 호환성을 유지하기 위한 체크리스트와 mixin.debug.export를 이용한 디버깅 방법, 그리고 일반적인 실수들을 상세히 설명합니다. 이 스킬은 Claude Code, Cursor, Codex 등 모든 AI 에이전트와 함께 사용할 수 있으며, Fabric Loader 0.15+ 및 Loom 1.12+ 환경을 지원합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/vvs1off/fabric-skills/tree/main/skills/fabric-mixins

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Fabric Mixin

Mixin은 로드 시점에 마인크래프트의 바이트코드를 패치합니다. 강력하지만 깨지기 쉽습니다. 모든 Mixin은 특정 메서드와 특정 형태가 다음 버전에도 여전히 존재하고, 다른 모드가 비호환적으로 변경하지 않을 것이라는 약속입니다. 가능한 한 적게, 가능한 한 좁게 작성하세요.

결정 순서 - Mixin을 작성하기 전에 시도할 것들

  1. Fabric API 이벤트 또는 레지스트리. Fabric API를 먼저 검색하세요: 생명주기 이벤트(ServerLifecycleEvents, ClientTickEvents), 플레이어 이벤트(UseBlockCallback, AttackEntityCallback, PlayerBlockBreakEvents), HUD(HudElementRegistry), 화면(ScreenEvents), 전리품(LootTableEvents), 명령어(CommandRegistrationCallback), 네트워킹, 아이템 툴팁, 리소스 리로드 리스너 등 더 많은 것들이 있습니다. https://github.com/FabricMC/fabric-api (각 모듈의 api 패키지)와 Fabric 문서를 참조하세요. 이벤트는 버전 간에 안정적이며 다른 모드와 자유롭게 구성됩니다.
  2. 바닐라 확장 포인트. 데이터 기반 콘텐츠(태그, 전리품 테이블, 레시피, 데이터 컴포넌트, 레지스트리)는 종종 코드 패치가 전혀 필요하지 않습니다.
  3. Accessor / Invoker Mixin - private 필드를 읽거나/쓰거나 private 메서드를 호출해야 할 때만 사용합니다.
  4. 클래스 트위커 (access widener) - final 클래스를 서브클래싱하거나, final 메서드를 오버라이드하거나, private/package-private 클래스를 참조해야 할 때 사용합니다.
  5. 인젝터 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 중 하나에만 선언되었거나 검증되지 않은 경우.

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

관련 스킬