메가 스택 뭉침 메커니즘 (MC 26.2)
이 문서는 Minecraft 26.2 버전의 Item Clumps에 대한 바닥 아이템 집약, 탐색 반경 연산, 64비트 오버플로 방지 및 힙 메모리 할당 최적화에 대한 포괄적인 분석을 제공합니다.
📊 기능 정보 상자
| 속성 | 세부 정보 |
|---|---|
| 시스템 이름 | 바닥 아이템 메가 스택 집약기 |
| 대상 클래스 | net.minecraft.world.entity.item.ItemEntity |
| 기본 뭉침 한도 | 9,999개 (최대 $2,147,483,647$개까지 설정 가능) |
| 탐색 반경 ($r$) | 수평 $1$ ~ $10$ 블록 (기본값: $1$ 블록) |
| 틱 연산 부하 | 정지된 아이템 $0.00\text{ ms}$ (setItem() 호출 시에만 작동) |
| 메모리 할당 | 탐색 및 흡수 시 객체 할당 제로 (copyWithCount(int)) |
| 제어 게임 규칙 | item_clumps:enable_clumping, item_clumps:max_clump_size, item_clumps:merge_radius |
⚙️ 메가 스택 뭉침 작동 원리
바닐라 Minecraft에서는 바닥에 떨어진 아이템이 틱 주기 동안 mergeWithNeighbours()를 호출하여 근처의 동일한 아이템을 탐색합니다. 그러나 바닐라는 대상 엔티티의 아이템 수량이 itemStack.getMaxStackSize()(일반적으로 64개, 엔더 진주나 달걀은 16개)를 초과하면 병합을 엄격히 금지합니다.
Item Clumps는 ItemEntityMixin.java를 통해 이 검사를 가로채어 데이터 컴포넌트 안전성을 완벽히 유지하면서 이 인위적인 제약을 해제합니다.
┌──────────────────────────────┐
│ ItemEntity A Ticks In │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Fast-Path Validation │
│ - Matching player target? │
│ - Identical DataComponents? │
└──────────────┬───────────────┘
│ (Pass)
▼
┌──────────────────────────────┐
│ In-Flight Magnet Check │
│ (Is either item magnetized?) │
└──────────────┬───────────────┘
│ (No)
▼
┌──────────────────────────────┐
│ 64-Bit Arithmetic Sum │
│ S = (long) A + (long) B │
└──────────────┬───────────────┘
│
┌───────────────┴───────────────┐
│ │
S <= maxClumpSize S > maxClumpSize
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Complete Merge │ │ Partial Merge │
│ Larger absorbs │ │ Fill entity A to │
│ smaller entity; │ │ maxClumpSize; │
│ Younger age kept. │ │ B keeps remainder.│
└───────────────────┘ └───────────────────┘📐 수학 공식 및 정밀 연산
1. 수평 히트박스(Bounding Box) 확장
바닐라는 매우 좁은 범위를 탐색합니다: $\text{AABB}_{\text{vanilla}} = \text{AABB}.\text{inflate}(0.5, 0.0, 0.5)$.
Item Clumps는 메모리 할당이 없는 @Redirect 주입을 사용하여 수평 히트박스를 동적으로 확장합니다:
$$\text{AABB}_{\text{clump}} = \text{boundingBox}.\text{inflate}(r, y, r)$$
여기서:
- $r = \text{DynamicGameRuleManager}.\text{getInt}(\text{level}, \text{MERGE_RADIUS}) \in [1, 10]$ 블록.
- $y = 0.0$ (서로 다른 층이나 깔때기 위의 아이템이 교차 뭉침되는 것을 방지하기 위해 수직 탐색 높이는 엄격히 유지됩니다).
2. 64비트 정수 합산 및 산술 오버플로 방지
대형 서버 환경에서 두 개의 대형 뭉치가 합쳐질 때(예: 최대 한도가 20억 개로 설정된 경우), 일반적인 32비트 int 덧셈은 음수로 오버플로되어($\text{Integer.MAX_VALUE} + 1 = -2,147,483,648$) 엔티티 상태가 손상될 수 있습니다.
Item Clumps는 64비트 정밀도로 합계를 계산합니다:
$$S = (\text{long}),\text{thisCount} + (\text{long}),\text{otherCount}$$
- 완전 흡수 ($S \le \text{maxClumpSize}$): $$\text{count}_{\text{merged}} = (\text{int}),S$$
- 부분 흡수 ($S > \text{maxClumpSize}$): $$\Delta = \text{maxClumpSize} - \text{thisCount}$$ $$\text{thisCount}' = \text{maxClumpSize}, \quad \text{otherCount}' = \text{otherCount} - \Delta$$
3. 컴포넌트 및 상태의 엄격한 일치
모든 데이터 컴포넌트(마법 부여, 내구도 손상, 사용자 정의 이름, 갑옷 장식, 물약 효과)가 비트 단위로 100% 일치하지 않는 한 아이템은 절대로 뭉쳐지지 않습니다:
$$\text{ItemStack}.\text{isSameItemSameComponents}(\text{stack}_A, \text{stack}_B) == \text{true}$$
💻 실제 Java 소스 코드 구현
Minecraft 26.2의 ItemEntityMixin.java 출처:
@Inject(method = "tryToMerge", at = @At("HEAD"), cancellable = true)
private void item_clumps$customMerge(ItemEntity other, CallbackInfo ci) {
ItemStack thisStack = this.getItem();
ItemStack otherStack = other.getItem();
// Fast-path exit before GameRule lookups
if (!Objects.equals(this.target, ((ItemEntityMixin)(Object)other).target) ||
!ItemStack.isSameItemSameComponents(thisStack, otherStack)) {
return;
}
if (!DynamicGameRuleManager.getBoolean(this.level(), ItemClumpsFabric.ENABLE_CLUMPING)) return;
// Magnet mod in-flight protection
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("magnet")) {
try {
java.lang.reflect.Method isMagnetizedMethod;
try {
isMagnetizedMethod = this.getClass().getMethod("ig_magnet$isMagnetized");
} catch (NoSuchMethodException e) {
isMagnetizedMethod = this.getClass().getMethod("ig$isMagnetized");
}
if ((boolean) isMagnetizedMethod.invoke(this) || (boolean) isMagnetizedMethod.invoke(other)) {
ci.cancel();
return;
}
} catch (Throwable ignored) {}
}
int thisCount = thisStack.getCount();
int otherCount = otherStack.getCount();
int maxClump = (ItemClumpsFabric.MAX_CLUMP_SIZE == null)
? thisStack.getMaxStackSize()
: DynamicGameRuleManager.getInt(this.level(), ItemClumpsFabric.MAX_CLUMP_SIZE);
long sum = (long) thisCount + (long) otherCount;
if (sum > (long) maxClump) {
int spaceLeft = maxClump - thisCount;
if (spaceLeft > 0) {
ItemStack thisCopy = thisStack.copyWithCount(maxClump);
this.setItem(thisCopy);
ItemStack otherCopy = otherStack.copyWithCount(otherCount - spaceLeft);
other.setItem(otherCopy);
this.pickupDelay = Math.max(this.pickupDelay, ((ItemEntityMixin)(Object)other).pickupDelay);
this.age = Math.min(this.age, ((ItemEntityMixin)(Object)other).age);
}
ci.cancel();
return;
}
// Full Merge: larger stack absorbs the smaller stack
if (otherCount < thisCount) {
ItemStack thisCopy = thisStack.copyWithCount((int) sum);
this.setItem(thisCopy);
this.pickupDelay = Math.max(this.pickupDelay, ((ItemEntityMixin)(Object)other).pickupDelay);
this.age = Math.min(this.age, ((ItemEntityMixin)(Object)other).age);
other.discard();
} else {
ItemStack otherCopy = otherStack.copyWithCount((int) sum);
other.setItem(otherCopy);
((ItemEntityMixin)(Object)other).pickupDelay = Math.max(((ItemEntityMixin)(Object)other).pickupDelay, this.pickupDelay);
((ItemEntityMixin)(Object)other).age = Math.min(((ItemEntityMixin)(Object)other).age, this.age);
this.discard();
}
ci.cancel();
}