メガスタック集約メカニズム (MC 26.2)
本書では、Minecraft 26.2 におけるアイテム集約、検索半径計算、64ビットオーバーフロー対策、ヒープ割り当て最適化の技術的・数学的解説を提供します。
📊 機能情報ボックス
| 項目 | 仕様 |
|---|---|
| システム名 | 地上アイテム・メガスタック集約エンジン |
| 対象クラス | net.minecraft.world.entity.item.ItemEntity |
| 初期結合上限 | 9,999 個 (最大 $2,147,483,647$ 個まで設定可能) |
| 探索半径 ($r$) | 水平 $1$ 〜 $10$ ブロック (初期値: $1$ ブロック) |
| Tick処理負荷 | 静止アイテムで $0.00\text{ ms}$ (setItem() 時のみ実行) |
| メモリ割り当て | 探索・吸収時のオブジェクト割り当てゼロ (copyWithCount(int)) |
| 制御ゲームルール | item_clumps:enable_clumping, item_clumps:max_clump_size, item_clumps:merge_radius |
⚙️ メガスタック集約の仕組み
バニラのMinecraftでは、ドロップされたアイテムは各tickで 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. 水平バウンディングボックスの拡張
バニラは非常に狭い範囲のみを探索します: $\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ビット整数加算ではオーバーフローして負の値になり ($\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();
}