物品掉落和寶箱生成
===
在本章節你會了解
---
- 什麼是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鼠填坑中(´◔ ∀◔\`)