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