---
# System prepended metadata

title: 物品掉落和寶箱生成

---

物品掉落和寶箱生成
===

在本章節你會了解
---
- 什麼是Loot Table
- 如何撰寫Loot Table
- 如何修改原版掉落機制

什麼是Loot Table
---
就如同我之前在[第一個方塊](https://hackmd.io/@immortalmice/H1voHSk_I)中提到的
方塊的物品掉落已經全面被一個叫 "Loot Table" 的東西接管了
雖然你依舊可以用覆寫`Block.getDrops()`來控制方塊的物品掉落，但這不是這篇文的重點所以這邊不多做說明。如果真的需要，你可以回去看[第一個方塊](https://hackmd.io/@immortalmice/H1voHSk_I)這篇文。

這個Loot Table的真面目其實就是一個Json檔
他會被放在`resource/data/modid/loot_tables`資料夾裡

事實上，這個Loot Table能控制的東西不只是方塊而已
簡單來說，上面提到的資料夾中可以有下面提到的這幾個資料夾和檔案
(下面的名字如果後綴沒有`.json`，那就是資料夾，反之則是檔案)

- `blocks` 控制方塊被破壞時的掉落物品
- `chests` 控制寶箱生成時裡面的物品
    - `village` 不同生成[結構](https://minecraft-zh.gamepedia.com/%E7%94%9F%E6%88%90%E7%BB%93%E6%9E%84)房子中寶箱生成時裡面的物品
- `entities` 控制實體被破壞/殺死時掉落的物品
- `gameplay` 其他，下面還有子資料夾
    - `fishing` 控制釣魚所獲得的物品，底下的Json檔分別是釣到魚、垃圾、寶物時的物品
    - `hero_of_the_village` 控制帶有[村莊英雄](https://minecraft-zh.gamepedia.com/%E6%9D%91%E5%BA%84%E8%8B%B1%E9%9B%84)的玩家被村民贈送禮物時的物品
    - `cat_morning_gift.json` 控制馴服的貓咪在早晨時贈送給玩家的物品
    - `fishing.json` 釣魚的總入口，裡面控制了釣到魚、垃圾和寶物的機率

如何撰寫Loot Table
---
關於Loot Table的格式，其實在[wiki](https://minecraft-zh.gamepedia.com/index.php?title=%E6%88%98%E5%88%A9%E5%93%81%E8%A1%A8&variant=zh-tw)中已經有非常詳細的介紹了

說真的，Wiki上的資料真的非常詳細
基本上如果你能看完，M鼠這邊沒有什麼東西可以告訴你了
但第一次看會覺得非常混亂，M鼠自己也看得很頭痛

因此這邊M鼠嘗試用一個新穎的方法來教大家
那就是用虛擬碼(Pseudocode)跑一次整個Loot Table的流程

下面的虛擬碼 **非常推薦** 搭配著原版的Loot Table Json檔當範例一起看
你可以自己找自己有興趣的，或是M鼠這邊推薦幾個Loot Table
- `entities/zombie`
- `entities/creeper`
- `entities/sheep/lime`
- `blocks/diamond_ore`
- `chests/nether_bridge`
- `gameplay/fishing`

以下是虛擬碼，**並不代表原版是真的完全照此執行**
```java=
/* root 為 Json 檔的根結點 */

for(root.pools 中的所有的池子 $pool){
    if($pool.conditions 中的所有條件都符合){
    
        for(次數 $pool.rolls + $pool.bonus_rolls){
            for($pool.entries 中的 $entry){
                if($entry.conditions 中所有條件都符合){
                    $finalWeight = $entry.weight + ($entry.quality * 玩家Luck值);
                }else{
                    $finalWeight = 0; //放棄此$entry
                }
            }
            
            取隨機數;
            以隨機數及所有 $entry 的 $finalWeight 為底抽出其中一個為 $selectedEntry;
            
            switch($selectedEntry.type){
            
                case minecraft:item:
                    取出 $selectedEntry.name 指定的物品;
                    break;
                    
                case minecraft:tag:
                    if($selectedEntry.expand){
                        隨機取出 $selectedEntry.name 中Tag的其中一個物品
                    }else{
                        取出 $selectedEntry.name 中Tag的所有物品
                    }
                    break;
                    
                case minecraft:loot_table:
                    直接使用 $selectedEntry.name 指定的Loot Table;
                    break;
                
                case minecraft:dynamic:
                    /* 這個在原版中並沒有被使用到 */
                    /* 但是你可以看一下函數 set_contents */
                    break;
                
                case minecraft:group:
                    /* 這個在原版中並沒有被使用到 */
                    取出 $selectedEntry.children 中
                    所有通過 $selectedEntry.children[n].conditions 的物品
                    break;
                    
                case minecraft:alternatives:
                    取出 $selectedEntry.children 中
                    第一個通過 $selectedEntry.children[n].conditions 的物品
                    break;
                    
                case minecraft:sequence:
                    /* 這個在原版中並沒有被使用到 */
                    取出 $selectedEntry.children 中
                    所有通過 $selectedEntry.children[n].conditions 的物品
                    直至第一個不通過就停止繼續取出
                    break;
            }
            
            對 $selectedEntry 階段抽出的物品依序進行 $selectedEntry.functions 中所有的函數
        }
        
        對 $pool 階段抽出的物品依序進行 $pool.functions 中所有的函數
    }
}

對所有抽出的物品依序進行 root.functions 中所有的函數
```

如果你對於哪一個東西有興趣，你可以直接搜尋原版的資料包
Ex. 試著對data/minecraft/loot_tables資料夾搜尋 `minecraft:alternatives` 看看吧

你會發現，上面有提到兩個很特別的東西，就是 `conditions` 和 `functions`
前者決定了這個節點該不該被採用，後者會對抽出的物品進行函數修改

所有可以用的 `conditions` 和 `functions` 都在[wiki](https://minecraft-zh.gamepedia.com/index.php?title=%E6%88%98%E5%88%A9%E5%93%81%E8%A1%A8&variant=zh-tw)有詳盡的介紹
這邊如果你已經能了解Loot Table Json檔的運行機制，wiki的介紹對你來說應該不是問題
M鼠這邊就不做多餘的解釋

如何修改原版掉落機制
---

以資料包來說，如果你寫的Json檔放在minecraft命名空間下
他就會覆蓋原版同名的Json檔，這是資料包的運作方式

但是，這個方式通常沒辦法滿足我們寫模組的的需求
試想以下的情況
1. 兩個模組都嘗試用這方式覆蓋了原版的Json檔，那最後到底是用誰的？
2. 你需要的是增加新的掉落物，而不是完全把原版的覆蓋掉

因此，從程式碼的層面來解決這問題會是比較萬用且穩定的
至於要怎麼做，其實很簡單，你需要的是訂閱`LootTableLoadEvent`這個事件

這邊M鼠就來做個~~飯粒~~範例
飯粒很簡單，讓所有的寶箱有機會包含布丁~

首先是訂閱`LootTableLoadEvent`事件，這事件是在Forge Bus上
所以在我們的`com.github.immortalmice.modtutorialim.bus.ForgeEventHandlers`加上程式碼

```java=
@SubscribeEvent
public static void onLootLoad(LootTableLoadEvent event){
    if(event.getTable().getParameterSet() == LootParameterSets.CHEST){

    }
}
```

你可以看到訂閱事件後，M鼠做了一個篩選
由於我們是要在寶箱中加入布丁，所以用`LootTable`的`LootParameterSet`判斷

所有原版的`LootParameterSet`都被放在`net.minecraft.world.storage.loot.LootParameterSets`
總共有以下幾個，事實上，他等同於Json檔裡root.type裡的值，代表此LootTable的類型
- `LootParameterSets.EMPTY`
- `LootParameterSets.CHEST`
- `LootParameterSets.COMMAND`
- `LootParameterSets.SELECTOR`
- `LootParameterSets.FISHING`
- `LootParameterSets.ENTITY`
- `LootParameterSets.GIFT`
- `LootParameterSets.ADVANCEMENT`
- `LootParameterSets.GENERIC`
- `LootParameterSets.BLOCK`

除了用`LootParameterSet`來篩選以外
你也可以用`LootTableLoadEvent.getName()`來獲得一個`ResourceLocation`
用這個`ResourceLocation`你可以確切的篩選出你要的那個LootTable，以下舉例
```java=
if(event.getName().equals(new ResourceLocation("minecraft", "grass"))){

}
```

好的，現在我們可以拿到從資料包讀取出來的原始LootTable，我們可以對它做些手腳

1. 如果你其實想要讓他什麼都不要生成

    好，這個超好解決，你只要取消事件(`Event.setCanceled(true)`)就好
    `LootTableLoadEvent`若被取消，會返回一個空的LootTable，相當於什麼都沒有
    
2. 你想要直接把整個LootTable替換掉
    
    我們首先來複習一下簡要的LootTable架構
    一個LootTable可以有多個Pool，每個Pool都會被執行抽選過
    一個Pool可以有多個Entry，只有其中一個Entry會被抽選到成為結果
    
    複習完架構後，你再來要決定你的新LootTable要從哪裡來
    
    - 從一個指定的Json來

        那麼你需要用`net.minecraft.world.storage.loot.LootPool.builder()`創建一個Pool
        在Pool裡加入一個用`net.minecraft.world.storage.loot.TableLootEntry.builder()`創建的Entry，傳入一個`ResourceLocation`就是你的Json檔位置
        
        如果你很聰明
        你應該會發現這邊的邏輯就是利用了Entry的Type為`minecraft:loot_table`
        你可以回去上面看虛擬碼中的`case: minecraft:loot_table`那一段
        
    - 直接由程式動態生成
        
        Well，那麼你就做好準備自己憑空用程式建立出一個LootTable囉
        為甚麼M鼠要這麼說？~~當然是因為這個很麻煩阿~~
        
        對於LootTable，用`net.minecraft.world.storage.loot.LootTable.Builder`創建
        然後用`LootTable.Builder.addLootPool()`加入一個或多個Pool
        
        上面的方法傳入一個`net.minecraft.world.storage.loot.LootPool.Builder`
        它可以從`LootPool.builder()`這個靜態方法獲得
        同時你也可以用這個Builder處理你的Pool相關設定
        最後用`LootPool.Builder.addEntry()`加入一個或多個Entry
        
        上面的方法傳入一個`net.minecraft.world.storage.loot.LootEntry.Builder<T>`
        這個Builder甚至`LootEntry`本身都是個虛擬類別(Abstract Class)
        所以你必須使用`LootEntry`的子類來獲得一個Builder
        有這些子類可以使用：
        - ================ StandaloneLootEntry ================
        - `net.minecraft.world.storage.loot.DynamicLootEntry`
        - `net.minecraft.world.storage.loot.EmptyLootEntry`
        - `net.minecraft.world.storage.loot.ItemLootEntry`
        - `net.minecraft.world.storage.loot.TableLootEntry`
        - `net.minecraft.world.storage.loot.TagLootEntry`
        - ================ ParentedLootEntry ==================
        - `net.minecraft.world.storage.loot.AlternativesLootEntry`
        - `net.minecraft.world.storage.loot.GroupLootEntry`
        - `net.minecraft.world.storage.loot.SequenceLootEntry`
        
        你會發現其實他們就是對應Entry的Type，可以回去上面看虛擬碼中的`switch`段落
        
        屬於`net.minecraft.world.storage.loot.StandaloneLootEntry`子類的Entry
        你都可以在類別中找到一個回傳`StandaloneLootEntry.Builder<?>`的靜態方法
        可惜的是這些靜態方法(Static Method)並沒有統一的名字，有些甚至還是Searge Name
        
        屬於`net.minecraft.world.storage.loot.ParentedLootEntry`子類的Entry
        只有`AlternativesLootEntry`有一個名字叫`builder`的靜態方法可以使用
        但另外兩個M鼠並沒有找到可以獲得Builder的方法
        由於這兩個在原版中其實是沒被使用到的，因此M鼠推斷應該是這功能沒有被完成
        *(如果M鼠有誤，請來信告知)*
        
        通過這樣一條龍的Builder~~地獄~~，你就可以創建出一個動態的LootTable了
        ~~ㄏㄏ，我就說很麻煩了吧~~
        
        最後，你只需要把這個新的LootTable用`LootTableLoadEvent.setTable()`直接替換
        
3. 如果你只是想加入新的一個或多個Pool

    要做的事和上面的有一些相似
    但你只要建立出一個或多個Pool就好了，不用到建立整個LootTable
    
    如何建立一個新的Pool，你可以看上面第二點的內容，這邊不再重複介紹
    有了`LootTable.Builder`後，直接呼叫`build`就可以獲得完整的Pool了
    最後用`LootTable.addPool()`方法加進去就可以了
    
M鼠要做的這個範例，是屬於上述的第三點
        
        

M鼠填坑中（´◔ ∀◔\`)