463 lines
17 KiB
Java
463 lines
17 KiB
Java
package com.jamesdpeters.minecraft.chests.interfaces;
|
|
|
|
import com.google.common.collect.Maps;
|
|
import com.jamesdpeters.minecraft.chests.ChestsPlusPlus;
|
|
import com.jamesdpeters.minecraft.chests.CraftingResult;
|
|
import com.jamesdpeters.minecraft.chests.api.ApiSpecific;
|
|
import com.jamesdpeters.minecraft.chests.crafting.Crafting;
|
|
import com.jamesdpeters.minecraft.chests.misc.Utils;
|
|
import com.jamesdpeters.minecraft.chests.serialize.Config;
|
|
import com.jamesdpeters.minecraft.chests.serialize.LocationInfo;
|
|
import com.jamesdpeters.minecraft.chests.storage.autocraft.AutoCraftingStorage;
|
|
import com.jamesdpeters.minecraft.chests.storage.chestlink.ChestLinkStorage;
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.Sound;
|
|
import org.bukkit.block.Block;
|
|
import org.bukkit.block.BlockFace;
|
|
import org.bukkit.block.Container;
|
|
import org.bukkit.block.Hopper;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.inventory.InventoryType;
|
|
import org.bukkit.inventory.Inventory;
|
|
import org.bukkit.inventory.InventoryHolder;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.inventory.Recipe;
|
|
import org.bukkit.inventory.RecipeChoice;
|
|
import org.bukkit.inventory.ShapedRecipe;
|
|
import org.bukkit.inventory.ShapelessRecipe;
|
|
import org.bukkit.scheduler.BukkitRunnable;
|
|
import org.bukkit.scheduler.BukkitTask;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.stream.Collectors;
|
|
|
|
public class VirtualCraftingHolder implements InventoryHolder {
|
|
|
|
private final Inventory inventory;
|
|
private final AutoCraftingStorage storage;
|
|
private BukkitRunnable guiTask;
|
|
private BukkitRunnable craftItemTask;
|
|
|
|
private ItemStack[][] recipeChoices = new ItemStack[9][];
|
|
private ItemStack result;
|
|
private final int[] recipeChoiceIndex = new int[9];
|
|
private boolean hasCompleteRecipe = false;
|
|
|
|
/**
|
|
* This gets set to true when updateCrafting() gets called on a 1 tick delay.
|
|
*/
|
|
private boolean isUpdatingRecipe = false;
|
|
|
|
private static final ItemStack AIR = new ItemStack(Material.AIR);
|
|
|
|
public VirtualCraftingHolder(AutoCraftingStorage storage) {
|
|
this.storage = storage;
|
|
inventory = Bukkit.createInventory(this, InventoryType.WORKBENCH, storage.getIdentifier());
|
|
}
|
|
|
|
@Override
|
|
public Inventory getInventory() {
|
|
return inventory;
|
|
}
|
|
|
|
|
|
public AutoCraftingStorage getStorage() {
|
|
return storage;
|
|
}
|
|
|
|
public void setCrafting(ShapelessRecipe shapelessRecipe) {
|
|
result = shapelessRecipe.getResult();
|
|
List<RecipeChoice> choiceList = shapelessRecipe.getChoiceList();
|
|
for (int i = 0; i < choiceList.size(); i++) {
|
|
RecipeChoice recipeChoice = choiceList.get(i);
|
|
if (recipeChoice instanceof RecipeChoice.MaterialChoice materialChoice) {
|
|
ItemStack[] choices = materialChoice.getChoices().stream().map(ItemStack::new).toArray(ItemStack[]::new);
|
|
recipeChoices[i] = choices;
|
|
}
|
|
}
|
|
setHasCompleteRecipe();
|
|
}
|
|
|
|
public void setCrafting(ShapedRecipe recipe) {
|
|
result = recipe.getResult();
|
|
int row = 0;
|
|
for (String r : recipe.getShape()) {
|
|
int col = 0;
|
|
for (char c : r.toCharArray()) {
|
|
RecipeChoice recipeChoice = recipe.getChoiceMap().get(c);
|
|
if (recipeChoice instanceof RecipeChoice.MaterialChoice materialChoice) {
|
|
ItemStack[] choices = materialChoice.getChoices().stream().map(ItemStack::new).toArray(ItemStack[]::new);
|
|
int i = (row * 3) + col;
|
|
recipeChoices[i] = choices;
|
|
}
|
|
col++;
|
|
}
|
|
row++;
|
|
}
|
|
setHasCompleteRecipe();
|
|
}
|
|
|
|
public void setCrafting(Recipe recipe, ItemStack[] matrix) {
|
|
if (recipe instanceof ShapedRecipe) setCrafting((ShapedRecipe) recipe);
|
|
else if (recipe instanceof ShapelessRecipe) setCrafting((ShapelessRecipe) recipe);
|
|
else {
|
|
// For ComplexRecipes or other implementations just use the result and original matrix for choices.
|
|
result = ApiSpecific.getNmsProvider().getCraftingProvider().craft(Bukkit.getWorlds().get(0), matrix).result();
|
|
for (int i = 0; i < matrix.length; i++) {
|
|
ItemStack item = matrix[i];
|
|
if (item != null) {
|
|
recipeChoices[i] = new ItemStack[]{item};
|
|
} else {
|
|
recipeChoices[i] = null;
|
|
}
|
|
}
|
|
setHasCompleteRecipe();
|
|
}
|
|
}
|
|
|
|
private void setHasCompleteRecipe() {
|
|
hasCompleteRecipe = true;
|
|
startCraftingItems();
|
|
}
|
|
|
|
public void resetChoices() {
|
|
recipeChoices = new ItemStack[9][];
|
|
result = null;
|
|
hasCompleteRecipe = false;
|
|
stopCraftingItems();
|
|
}
|
|
|
|
public void updateCrafting() {
|
|
var crafting = Arrays.copyOfRange(inventory.getContents(),1, inventory.getContents().length);
|
|
|
|
Recipe recipe = Crafting.getRecipe(storage.getOwner().getPlayer(), crafting);
|
|
getStorage().setRecipe(recipe, crafting); // Only store the crafting matrix if the recipe is valid
|
|
resetChoices();
|
|
|
|
if (recipe != null) {
|
|
setCrafting(recipe, crafting);
|
|
playSound(Sound.BLOCK_NOTE_BLOCK_CHIME, 0.5f, 1f);
|
|
} else {
|
|
stopCraftingItems();
|
|
}
|
|
|
|
isUpdatingRecipe = false;
|
|
updateGUI();
|
|
storage.onItemDisplayUpdate(result);
|
|
}
|
|
|
|
private void playSound(Sound sound, float volume, float pitch) {
|
|
storage.getInventory().getViewers().forEach(humanEntity -> {
|
|
humanEntity.getWorld().playSound(humanEntity.getLocation(), sound, volume, pitch);
|
|
});
|
|
}
|
|
|
|
public VirtualCraftingHolder setUpdatingRecipe(boolean updatingRecipe) {
|
|
isUpdatingRecipe = updatingRecipe;
|
|
return this;
|
|
}
|
|
|
|
//Start and stop animation based on if the inventory is open.
|
|
public void startAnimation() {
|
|
guiTask = new UpdateTask();
|
|
}
|
|
|
|
public void stopAnimation() {
|
|
if (guiTask != null) guiTask.cancel();
|
|
}
|
|
|
|
private class UpdateTask extends BukkitRunnable {
|
|
BukkitTask task;
|
|
|
|
public UpdateTask() {
|
|
task = runTaskTimer(ChestsPlusPlus.PLUGIN, 1, 15);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
updateGUI();
|
|
}
|
|
}
|
|
|
|
public void updateGUI() {
|
|
inventory.setItem(0, result);
|
|
if (hasCompleteRecipe && !isUpdatingRecipe) {
|
|
for (int i = 0; i < 9; i++) {
|
|
ItemStack[] choices = recipeChoices[i];
|
|
if (choices != null) {
|
|
int index = recipeChoiceIndex[i];
|
|
ItemStack choice;
|
|
if (index < choices.length) {
|
|
choice = choices[index];
|
|
} else {
|
|
recipeChoiceIndex[i] = 0;
|
|
choice = choices[0];
|
|
}
|
|
inventory.setItem(i + 1, choice);
|
|
recipeChoiceIndex[i]++;
|
|
} else {
|
|
inventory.setItem(i + 1, null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void startCraftingItems() {
|
|
if (craftItemTask == null || craftItemTask.isCancelled()) craftItemTask = new CraftItems();
|
|
}
|
|
|
|
public void stopCraftingItems() {
|
|
if (craftItemTask != null) craftItemTask.cancel();
|
|
}
|
|
|
|
private class CraftItems extends BukkitRunnable {
|
|
BukkitTask task;
|
|
|
|
CraftItems() {
|
|
task = runTaskTimer(ChestsPlusPlus.PLUGIN, 1, 20);
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
craftItem();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method will craft an item if a chest above contains the correct amount of materials
|
|
* And there is a hopper below.
|
|
*/
|
|
public void craftItem() {
|
|
for (LocationInfo location : storage.getLocations()) {
|
|
if (!Utils.isLocationChunkLoaded(location.getLocation())) continue;
|
|
Block block = location.getLocation().getBlock();
|
|
Block blockBelow = block.getRelative(BlockFace.DOWN);
|
|
Block blockAbove = block.getRelative(BlockFace.UP);
|
|
|
|
Inventory output;
|
|
|
|
if (blockBelow.getState() instanceof Hopper hopper) {
|
|
if (blockBelow.isBlockPowered() || blockBelow.isBlockIndirectlyPowered()) {
|
|
continue; //If hopper is powered no crafting should happen.
|
|
}
|
|
output = hopper.getInventory();
|
|
} else {
|
|
output = getInventory(blockBelow);
|
|
//If there is no output crafting isn't possible, so skip this location.
|
|
if (output == null) continue;
|
|
//If crafting table is powered output into container is possible.
|
|
if (!block.isBlockPowered()) continue;
|
|
}
|
|
|
|
List<Inventory> inventories = new ArrayList<>();
|
|
Utils.addIfNotNull(inventories, getInventory(blockAbove));
|
|
Utils.addIfNotNull(inventories, getInventory(block.getRelative(BlockFace.NORTH)));
|
|
Utils.addIfNotNull(inventories, getInventory(block.getRelative(BlockFace.EAST)));
|
|
Utils.addIfNotNull(inventories, getInventory(block.getRelative(BlockFace.SOUTH)));
|
|
Utils.addIfNotNull(inventories, getInventory(block.getRelative(BlockFace.WEST)));
|
|
|
|
boolean didCraft = craftItem(inventories, output, getInventory());
|
|
|
|
//Play sound if crafting occured.
|
|
if (didCraft) {
|
|
if (location.getLocation().getWorld() != null) {
|
|
location.getLocation().getWorld().playSound(location.getLocation(), Sound.BLOCK_DISPENSER_DISPENSE, 0.25f, 1f);
|
|
if (output.getHolder() instanceof VirtualInventoryHolder) {
|
|
((VirtualInventoryHolder) output.getHolder()).getStorage().updateDisplayItem();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private Inventory getInventory(Block block) {
|
|
Inventory inventory = null;
|
|
if (block.getState() instanceof Container) {
|
|
ChestLinkStorage storage = Config.getChestLink().getStorage(block.getLocation());
|
|
//Check if a ChestLink exists above the CraftingTable and if the owner of the CraftingTable has permission to access that Chest.
|
|
if (storage != null) {
|
|
if (storage.hasPermission(this.storage.getOwner())) {
|
|
inventory = storage.getInventory();
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
inventory = ((Container) block.getState()).getInventory();
|
|
}
|
|
}
|
|
return inventory;
|
|
}
|
|
|
|
private int getRecipeChoiceAmount() {
|
|
int max = 0;
|
|
for (ItemStack[] recipeChoice : recipeChoices) {
|
|
if (recipeChoice != null && recipeChoice.length > max){
|
|
max = recipeChoice.length;
|
|
}
|
|
}
|
|
return max;
|
|
}
|
|
|
|
/**
|
|
* Finds a valid recipe matrix for the currently selected recipe.
|
|
* Removes the items from the inventories provided.
|
|
* @param inventories
|
|
* @return a valid recipe matrix selected from the inventories provided.
|
|
*/
|
|
private ItemStack[] getRecipeMatrix(List<Inventory> inventories) {
|
|
|
|
// Loop through recipe choice array to find recipeChoice index.
|
|
int recipeChoicesAmount = getRecipeChoiceAmount();
|
|
|
|
for (int recipeChoiceIndex = 0; recipeChoiceIndex < recipeChoicesAmount; recipeChoiceIndex++) {
|
|
|
|
// Store each item selected for the recipe.
|
|
// This is used to retrieve the actual result taking into account meta data, such as repairing items.
|
|
ItemStack[] tempRecipe = Utils.createAirList(9);
|
|
int recipeIndex = 0;
|
|
|
|
// Need a new copy of the inventories to test for each recipe choice.
|
|
List<Inventory> tempInvs = Utils.copyInventoryList(inventories);
|
|
|
|
// Bukkit.broadcastMessage("Recipe choices in loop: "+ Arrays.deepToString(recipeChoices));
|
|
|
|
// Loops over each slot of the matrix
|
|
for (ItemStack[] choices : recipeChoices) {
|
|
|
|
// If there's no recipe choice at this index skip and set the matrix pos to null
|
|
if (choices == null){
|
|
tempRecipe[recipeIndex] = null;
|
|
recipeIndex++;
|
|
|
|
} else { // Otherwise check for a valid item for this recipe choice.
|
|
|
|
// Select the current recipeChoice
|
|
ItemStack choice = choices[recipeChoiceIndex];
|
|
|
|
boolean foundMatch = false;
|
|
for (Inventory tempInv : tempInvs) {
|
|
int index = tempInv.first(choice.getType());
|
|
|
|
if (index != -1) {
|
|
ItemStack item = tempInv.getItem(index);
|
|
|
|
if (item != null) {
|
|
// If a valid item has been found in one of the inventories.
|
|
ItemStack selectedItem = item.clone();
|
|
|
|
// if (selectedItem.getType())
|
|
item.setAmount(item.getAmount() - 1);
|
|
tempInv.setItem(index, item);
|
|
|
|
selectedItem.setAmount(1);
|
|
tempRecipe[recipeIndex] = selectedItem;
|
|
|
|
recipeIndex++;
|
|
foundMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//If no match found
|
|
if (!foundMatch) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Completed the matrix
|
|
if ((recipeIndex >= recipeChoices.length)) {
|
|
|
|
// Move temp invs to input invs.
|
|
for (int i = 0; i < tempInvs.size(); i++) {
|
|
moveTempInv(tempInvs.get(i), inventories.get(i));
|
|
}
|
|
|
|
return tempRecipe;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private boolean craftItem(List<Inventory> inputs, Inventory output, Inventory craftingInventory) {
|
|
boolean sameInv = false;
|
|
Inventory sameInventory = null;
|
|
|
|
// Create a copy of the real inventories and decide if any of the inputs match the output.
|
|
List<Inventory> tempInvs = new ArrayList<>();
|
|
|
|
// Remove duplicate inventories from list.
|
|
inputs = inputs.stream().distinct().collect(Collectors.toList());
|
|
|
|
for (Inventory inv : inputs) {
|
|
Inventory tempInv = Utils.copyInventory(inv);
|
|
tempInvs.add(tempInv);
|
|
if (inv.equals(output)) {
|
|
sameInv = true;
|
|
sameInventory = tempInv;
|
|
}
|
|
}
|
|
|
|
ItemStack[] recipe = getRecipeMatrix(tempInvs);
|
|
// null recipe means no recipe was found from the inventories.
|
|
if (recipe == null) return false;
|
|
|
|
// Use NMS to get the real result considering meta data etc.
|
|
CraftingResult craftingResult = Crafting.craft(storage.getOwner().getPlayer(), recipe);
|
|
|
|
//TODO
|
|
// Recipe recipeActual = Crafting.getRecipe(storage.getOwner().getPlayer(), recipe);
|
|
// CraftingInventoryImpl craftingInventoryImpl = new CraftingInventoryImpl(craftingInventory, craftingResult.getResult(), recipe, recipeActual);
|
|
// InventoryViewImpl inventoryView = new InventoryViewImpl(craftingInventory, output, ApiSpecific.getNmsProvider().getNPCProvider().createHumanEntity());
|
|
// PrepareItemCraftEvent itemCraftEvent = new PrepareItemCraftEvent(craftingInventoryImpl, inventoryView, false);
|
|
// Bukkit.getPluginManager().callEvent(itemCraftEvent);
|
|
|
|
if (craftingResult.result() == null) return false;
|
|
|
|
//If we reach here there are enough materials so check for space in the Hopper and update inventory.
|
|
//Check if output and input are the same inventory to avoid duplication.
|
|
Inventory tempOutput = sameInv ? sameInventory : Utils.copyInventory(output);
|
|
HashMap<Integer, ItemStack> map = tempOutput.addItem(craftingResult.result());
|
|
|
|
boolean isEmpty = Arrays.stream(craftingResult.matrixResult())
|
|
.anyMatch(itemStack -> (itemStack == null || itemStack.getType() == Material.AIR));
|
|
|
|
// Add any leftover items from the recipe e.g buckets.
|
|
HashMap<Integer, ItemStack> craftingMatrixLeftOvers =
|
|
isEmpty ? Maps.newHashMap()
|
|
: tempOutput.addItem(craftingResult.matrixResult());
|
|
|
|
//If result fits into output copy over the temporary inventories.
|
|
if (map.isEmpty() && craftingMatrixLeftOvers.isEmpty()) {
|
|
for (int i = 0; i < tempInvs.size(); i++) {
|
|
moveTempInv(tempInvs.get(i), inputs.get(i));
|
|
}
|
|
if (!sameInv) moveTempInv(tempOutput, output);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void moveTempInv(Inventory tempInv, Inventory realInv) {
|
|
ItemStack[] contents = new ItemStack[realInv.getSize()];
|
|
System.arraycopy(tempInv.getContents(), 0, contents, 0, realInv.getSize());
|
|
realInv.setContents(contents);
|
|
}
|
|
|
|
public void forceUpdateInventory() {
|
|
inventory.getViewers().forEach(humanEntity -> {
|
|
if (humanEntity instanceof Player) {
|
|
((Player) humanEntity).updateInventory();
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|