Minecraft把物品堆疊上限設為64是怎麼做到的
Lycohinya 有一個叫收納符的東西,拿來把背包或容器裡完全相同的物品整理成盡量少的堆疊。終界珍珠原版一組 16 顆,藥水一格一瓶,鎬子一格一把;收納符的設定上限是 64,所以它們只要真的完全相同,就應該可以往上疊。
這個「完全相同」直接交給 Bukkit 的 ItemStack.isSimilar() 判斷。它會比較材質跟整份 ItemMeta,但忽略數量;耐久、附魔、自訂名稱或 max_stack_size 只要有一項不同,就不會分到同一組。我不想另外維護一張「哪些東西可以疊」的清單,Minecraft 每加一種物品我就得回來補,遲早漏。
整理邏輯看起來也很單純:把同組物品的 amount 加總,再按照 64 顆一格寫回去。
然後東西不見了。
四把鎬子最後只剩一把
第一次重現時,我拿四把完全相同的鑽石鎬放進容器,用收納符整理。結果不是 count:4,而是只剩一把。終界珍珠兩疊各 16 顆,整理完只剩 16 顆。
客戶端顯示錯誤還比較好處理,但用 /data get 讀伺服器端 NBT 也是一樣。多出來的三把鎬子跟 16 顆珍珠是真的沒了。
原本的理解是,ItemStack.amount 既然可以填整數,那把它改成 4 或 32,再寫進 Inventory 就好。實際上物品身上還有另一個數字:max_stack_size。
鎬子的 amount 可以暫時是 4,鎬子自己申報的堆疊上限仍然是 1。容器接到這份 ItemStack 時看到 4 > 1,最後留下符合上限的那一把。終界珍珠也是同一件事,只是它的上限是 16。
amount 是這一疊現在有幾個,max_stack_size 是這件物品允許一疊最多有幾個。只改前者,沒有改到規則。
先把物品遺失停住
第一輪修正先把合併結果夾回 base.maxStackSize。鎬子不再消失,因為根本不合併;32 顆終界珍珠也會維持兩疊 16。
這個版本至少不會吃玩家的東西,但收納符原本要做的事也一起被拿掉了。
第二輪換成 Inventory.setContents(),想避開逐格呼叫 setItem() 時的限制。四把鎬子還是剩一把,32 顆珍珠還是剩 16 顆。問題不在選了哪一個 Inventory 寫入方法,兩條路最後都會檢查物品自己的堆疊上限。
那時候繼續找另一條「比較不會管我」的寫入路徑已經沒什麼意義了。就算找到,下一次容器存檔、玩家撿起物品或客戶端同步時還是可能再被修回去。這種繞法大概只是在幫之後的自己藏一顆地雷。
堆疊上限在物品上
Minecraft 1.20.5 之後,物品的很多屬性改成 data components,其中一個就是 minecraft:max_stack_size。Paper 在 ItemMeta 上提供了 setMaxStackSize(),可以直接改這件物品自己申報的堆疊上限。
最後真正需要的程式碼只有這幾行:
private fun withRaisedStackSize(base: ItemStack, cap: Int): ItemStack {
val stack = base.clone()
val meta = stack.itemMeta ?: return stack
meta.setMaxStackSize(maxOf(cap, base.maxStackSize))
stack.itemMeta = meta
return stack
}
ItemMeta 拿到的是一份要改完再塞回 ItemStack 的資料,所以最後那行不能漏。上限改成 64 之後,鎬子的 getMaxStackSize() 就會回 64,容器看到 amount=4 也不再覺得這是一份超出規則的物品。
整理時也不需要看到什麼都改成 64:
val template =
if (total > base.maxStackSize) withRaisedStackSize(base, cap)
else base
兩疊終界珍珠是 10+6,合計沒有超過原版的 16,就照原版物品合併,不加任何自訂元件。16+16 才把上限提高,再合成一疊 32。原本就能正常堆疊的東西不要多改,之後少一種自己製造出來的物品差異。
這個元件會跟著 ItemStack 保存。依 data component 的行為,拆開後的鎬子預期仍會保留提高過的堆疊上限;這一段沒有另外做「拆開再讀 component」的實測。我也沒有做一套「離開收納符就還原」的追蹤,玩家拿到的就是一件上限為 64 的物品。硬要讓它只在某個容器裡暫時成立,維護成本比這個功能本身還大。
代價也在這裡:提高過上限的鎬跟剛生成的原版鎬,ItemMeta 已經不同,isSimilar() 不會把它們當成同一組。這篇處理的是「同一批完全相同的物品怎麼合法疊起來」,沒有再做一層忽略特定元件的正規化。要忽略哪些、又不能誤吞耐久或附魔差異,會變成另一個功能。
我怎麼確認它真的沒有再吃東西
這次測試沒有看客戶端背包格數就算了。玩家背包這條路徑先用 /item replace 把物品放進兩個獨立欄位,整理後再用 /data get entity 讀伺服器端實際資料。容器則由 RCON 用 data merge block 寫好四把鎬子跟兩疊珍珠,讓測試帳號真的拿收納符右鍵箱子,最後用 /data get block 核對:
- 兩把鑽石鎬:兩格
count:1→ 一格count:2 - 四把鑽石鎬放進容器:四格
count:1→ 一格count:4 - 終界珍珠 10+6:一格
count:16,不提高上限 - 終界珍珠 16+16:一格
count:32,沒有少掉 16 顆
最後也在正式服重新部署並自己操作過。這次四把都還在。
其實 Paper 的 API 就在那裡。前面一直在換 Inventory 的寫入方式,是因為我把問題想成「怎麼讓容器接受超量物品」,所以一直盯著錯的地方。max_stack_size 本來就跟著物品走。
如果你也想讓自己的伺服器有這個功能,不想自己再寫一套,InventoryStacks 是別人已經做好的版本,可以直接拿來用。