package slimeknights.tconstruct.library.utils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import gnu.trove.map.TIntIntMap;
import gnu.trove.map.hash.TIntIntHashMap;
import gnu.trove.procedure.TIntIntProcedure;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.util.text.translation.I18n;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import slimeknights.mantle.util.RecipeMatch;
import slimeknights.tconstruct.library.TinkerRegistry;
import slimeknights.tconstruct.library.Util;
import slimeknights.tconstruct.library.events.TinkerEvent;
import slimeknights.tconstruct.library.materials.Material;
import slimeknights.tconstruct.library.modifiers.IModifier;
import slimeknights.tconstruct.library.modifiers.TinkerGuiException;
import slimeknights.tconstruct.library.tinkering.IRepairable;
import slimeknights.tconstruct.library.tinkering.MaterialItem;
import slimeknights.tconstruct.library.tinkering.PartMaterialType;
import slimeknights.tconstruct.library.tinkering.TinkersItem;
import slimeknights.tconstruct.library.tools.IToolPart;
import slimeknights.tconstruct.library.tools.Pattern;
import slimeknights.tconstruct.library.tools.ToolCore;
import slimeknights.tconstruct.library.traits.AbstractTrait;
import slimeknights.tconstruct.library.traits.ITrait;
import slimeknights.tconstruct.tools.TinkerTools;
public final class ToolBuilder {
private static Logger log = Util.getLogger("ToolBuilder");
private ToolBuilder() {
}
public static ItemStack tryBuildTool(ItemStack[] stacks, String name) {
return tryBuildTool(stacks, name, TinkerRegistry.getTools());
}
/**
* Takes an array of Itemstacks and tries to build a tool with it. Amount of itemstacks has to match exactly.
*
* @param stacks Input.
* @return The built tool or null if none could be built.
*/
public static ItemStack tryBuildTool(ItemStack[] stacks, String name, Collection<ToolCore> possibleTools) {
int length = -1;
ItemStack[] input;
// remove trailing nulls
for(int i = 0; i < stacks.length; i++) {
if(stacks[i] == null) {
if(length < 0) {
length = i;
}
}
else if(length >= 0) {
// incorrect input. gap with null in the stacks passed
return null;
}
}
if(length < 0) {
return null;
}
input = Arrays.copyOf(stacks, length);
for(Item item : possibleTools) {
if(!(item instanceof ToolCore)) {
continue;
}
ItemStack output = ((ToolCore) item).buildItemFromStacks(input);
if(output != null) {
// name the item
if(name != null && !name.isEmpty()) {
output.setStackDisplayName(name);
}
return output;
}
}
return null;
}
/**
* Adds the trait to the tag, taking max-count and already existing traits into account.
*
* @param rootCompound The root compound of the item
* @param trait The trait to add.
* @param color The color used on the tooltip. Will not be used if the trait already exists on the tool.
*/
public static void addTrait(NBTTagCompound rootCompound, ITrait trait, int color) {
// only registered traits allowed
if(TinkerRegistry.getTrait(trait.getIdentifier()) == null) {
log.error("addTrait: Trying to apply unregistered Trait {}", trait.getIdentifier());
return;
}
IModifier modifier = TinkerRegistry.getModifier(trait.getIdentifier());
if(modifier == null || !(modifier instanceof AbstractTrait)) {
log.error("addTrait: No matching modifier for the Trait {} present", trait.getIdentifier());
return;
}
AbstractTrait traitModifier = (AbstractTrait) modifier;
NBTTagCompound tag = new NBTTagCompound();
NBTTagList tagList = TagUtil.getModifiersTagList(rootCompound);
int index = TinkerUtil.getIndexInList(tagList, traitModifier.getModifierIdentifier());
if(index < 0) {
traitModifier.updateNBTforTrait(tag, color);
tagList.appendTag(tag);
TagUtil.setModifiersTagList(rootCompound, tagList);
}
else {
tag = tagList.getCompoundTagAt(index);
}
traitModifier.applyEffect(rootCompound, tag);
}
public static ItemStack tryRepairTool(ItemStack[] stacks, ItemStack toolStack, boolean removeItems) {
if(toolStack == null || !(toolStack.getItem() instanceof IRepairable)) {
return null;
}
// obtain a working copy of the items if the originals shouldn't be modified
if(!removeItems) {
stacks = Util.copyItemStackArray(stacks);
}
return ((IRepairable) toolStack.getItem()).repair(toolStack, stacks);
}
/**
* Takes a tool and an array of itemstacks and tries to modify the tool with those.
* If removeItems is true, the items used in the process will be removed from the array.
*
* @param input Items to modify the tool with
* @param toolStack The tool
* @param removeItems If true the applied items will be removed from the array
* @return The modified tool or null if something went wrong or no modifier applied.
* @throws TinkerGuiException Thrown when not matching modifiers could be applied. Contains extra-information why the process failed.
*/
public static ItemStack tryModifyTool(ItemStack[] input, ItemStack toolStack, boolean removeItems)
throws TinkerGuiException {
ItemStack copy = toolStack.copy();
// obtain a working copy of the items if the originals shouldn't be modified
ItemStack[] stacks = Util.copyItemStackArray(input);
ItemStack[] usedStacks = Util.copyItemStackArray(input);
Set<IModifier> appliedModifiers = Sets.newHashSet();
for(IModifier modifier : TinkerRegistry.getAllModifiers()) {
RecipeMatch.Match match;
do {
match = modifier.matches(stacks);
ItemStack backup = copy.copy();
// found a modifier that is applicable. Try to apply the match
if(match != null) {
// we need to apply the whole match
while(match.amount > 0) {
TinkerGuiException caughtException = null;
boolean canApply = false;
try {
canApply = modifier.canApply(copy, toolStack);
} catch(TinkerGuiException e) {
caughtException = e;
}
// but can it be applied?
if(canApply) {
modifier.apply(copy);
appliedModifiers.add(modifier);
match.amount--;
}
else {
// materials would allow another application, but modifier doesn't
// if we have already applied another modifier we cancel the whole thing to prevent situations where
// only a part of the modifiers gets applied. either all or none.
// if we have a reason, rather tell the player that
if(caughtException != null && !appliedModifiers.contains(modifier)) {
throw caughtException;
}
copy = backup;
RecipeMatch.removeMatch(stacks, match);
break;
}
}
if(match.amount == 0) {
RecipeMatch.removeMatch(stacks, match);
RecipeMatch.removeMatch(usedStacks, match);
}
}
} while(match != null);
}
// check if all itemstacks were touched - otherwise there's an invalid item in the input
for(int i = 0; i < input.length; i++) {
if(input[i] != null && ItemStack.areItemStacksEqual(input[i], stacks[i])) {
if(!appliedModifiers.isEmpty()) {
String error =
I18n.translateToLocalFormatted("gui.error.no_modifier_for_item", input[i].getDisplayName());
throw new TinkerGuiException(error);
}
return null;
}
}
// update output itemstacks
if(removeItems) {
for(int i = 0; i < input.length; i++) {
if(input[i] == null) {
continue;
}
// stacks might be null because stacksize got 0 during processing, we have to reflect that in the input
// so the caller can identify that
if(usedStacks[i] == null) {
input[i].stackSize = 0;
}
else {
input[i].stackSize = usedStacks[i].stackSize;
}
}
}
if(!appliedModifiers.isEmpty()) {
// always rebuild tinkers items to ensure consistency and find problems earlier
if(copy.getItem() instanceof TinkersItem) {
NBTTagCompound root = TagUtil.getTagSafe(copy);
rebuildTool(root, (TinkersItem) copy.getItem());
copy.setTagCompound(root);
}
return copy;
}
return null;
}
/**
* Takes a tool and toolparts and replaces the parts inside the tool with the given ones.
* Toolparts have to be applicable to the tool. Toolparts must not be duplicates of currently used parts.
*
* @param toolStack The tool to replace the parts in
* @param toolPartsIn The toolparts.
* @param removeItems If true the applied items will be removed from the array
* @return The tool with the replaced parts or null if the conditions have not been met.
*/
public static ItemStack tryReplaceToolParts(ItemStack toolStack, final ItemStack[] toolPartsIn, final boolean removeItems)
throws TinkerGuiException {
if(toolStack == null || !(toolStack.getItem() instanceof TinkersItem)) {
return null;
}
// we never modify the original. Caller can remove all of them if we return a result
List<ItemStack> inputItems = new ArrayList<>(Arrays.asList(Util.copyItemStackArray(toolPartsIn)));
if(!TinkerEvent.OnToolPartReplacement.fireEvent(inputItems, toolStack)) {
// event cancelled
return null;
}
final ItemStack[] toolParts = inputItems.toArray(new ItemStack[inputItems.size()]);
TIntIntMap assigned = new TIntIntHashMap();
TinkersItem tool = (TinkersItem) toolStack.getItem();
// materiallist has to be copied because it affects the actual NBT on the tool if it's changed
final NBTTagList materialList = TagUtil.getBaseMaterialsTagList(toolStack).copy();
// assing each toolpart to a slot in the tool
for(int i = 0; i < toolParts.length; i++) {
ItemStack part = toolParts[i];
if(part == null) {
continue;
}
if(!(part.getItem() instanceof IToolPart)) {
// invalid item for toolpart replacement
return null;
}
int candidate = -1;
// find an applicable slot in the tool structure corresponding to the toolparts position
List<PartMaterialType> pms = tool.getRequiredComponents();
for(int j = 0; j < pms.size(); j++) {
PartMaterialType pmt = pms.get(j);
String partMat = ((IToolPart) part.getItem()).getMaterial(part).getIdentifier();
String currentMat = materialList.getStringTagAt(j);
// is valid and not the same material?
if(pmt.isValid(part) && !partMat.equals(currentMat)) {
// part not taken up by previous part already?
if(!assigned.valueCollection().contains(j)) {
candidate = j;
// if a tool has multiple of the same parts we may want to replace another one as the currently selected
// for that purpose we only allow to overwrite the current selection if the input slot is a later one than the current one
if(i <= j) {
break;
}
}
}
}
// no assignment found for a part. Invalid input.
if(candidate < 0) {
return null;
}
assigned.put(i, candidate);
}
// did we assign nothing?
if(assigned.isEmpty()) {
return null;
}
// We now know which parts to replace with which inputs. Yay. Now we only have to do so.
// to do so we simply switch out the materials used and rebuild the tool
assigned.forEachEntry((i, j) -> {
String mat = ((IToolPart) toolParts[i].getItem()).getMaterial(toolParts[i]).getIdentifier();
materialList.set(j, new NBTTagString(mat));
if(removeItems) {
if(i < toolPartsIn.length && toolPartsIn[i] != null) {
toolPartsIn[i].stackSize--;
}
}
return true;
});
// check that each material is still compatible with each modifier
TinkersItem tinkersItem = (TinkersItem) toolStack.getItem();
ItemStack copyToCheck = tinkersItem.buildItem(TinkerUtil.getMaterialsFromTagList(materialList));
// this includes traits
NBTTagList modifiers = TagUtil.getBaseModifiersTagList(toolStack);
for(int i = 0; i < modifiers.tagCount(); i++) {
String id = modifiers.getStringTagAt(i);
IModifier mod = TinkerRegistry.getModifier(id);
// will throw an exception if it can't apply
if(mod != null && !mod.canApply(copyToCheck, copyToCheck)) {
throw new TinkerGuiException();
}
}
ItemStack output = toolStack.copy();
TagUtil.setBaseMaterialsTagList(output, materialList);
NBTTagCompound tag = TagUtil.getTagSafe(output);
rebuildTool(tag, (TinkersItem) output.getItem());
output.setTagCompound(tag);
// check if the output has enough durability. we only allow it if the result would not be broken
if(output.getItemDamage() > output.getMaxDamage()) {
String error = I18n.translateToLocalFormatted("gui.error.not_enough_durability", output.getItemDamage() - output.getMaxDamage());
throw new TinkerGuiException(error);
}
return output;
}
/**
* Takes a pattern and itemstacks and crafts the materialitem of the pattern out of it.
* The output consists of an ItemStack[2] array that contains the part in the first slot and eventual leftover output in the 2nd one.
* The itemstacks have to match at least 1 material.
* If multiple materials match, matches with multiple items are preferred.
* Otherwise the first match will be taken.
*
* @param pattern Input-pattern. Has to be a Pattern.
* @param materialItems The Itemstacks to craft the item out of
* @param removeItems If true the match will be removed from the passed items
* @return ItemStack[2] Array containing the built item in the first slot and eventual secondary output in the second one. Null if no item could be built.
*/
public static ItemStack[] tryBuildToolPart(ItemStack pattern, ItemStack[] materialItems, boolean removeItems)
throws TinkerGuiException {
Item itemPart = Pattern.getPartFromTag(pattern);
if(itemPart == null || !(itemPart instanceof MaterialItem) || !(itemPart instanceof IToolPart)) {
String error = I18n.translateToLocalFormatted("gui.error.invalid_pattern");
throw new TinkerGuiException(error);
}
IToolPart part = (IToolPart) itemPart;
if(!removeItems) {
materialItems = Util.copyItemStackArray(materialItems);
}
// find the material from the input
RecipeMatch.Match match = null;
Material foundMaterial = null;
for(Material material : TinkerRegistry.getAllMaterials()) {
// craftable?
if(!material.isCraftable()) {
continue;
}
RecipeMatch.Match newMatch = material.matches(materialItems, part.getCost());
if(newMatch == null) {
continue;
}
// we found a match, yay
if(match == null) {
match = newMatch;
foundMaterial = material;
// is it more complex than the old one?
}
}
// nope, no material
if(match == null) {
return null;
}
ItemStack output = ((MaterialItem) itemPart).getItemstackWithMaterial(foundMaterial);
if(output == null) {
return null;
}
if(output.getItem() instanceof IToolPart && !((IToolPart) output.getItem()).canUseMaterial(foundMaterial)) {
return null;
}
RecipeMatch.removeMatch(materialItems, match);
// check if we have secondary output
ItemStack secondary = null;
int leftover = (match.amount - part.getCost()) / Material.VALUE_Shard;
if(leftover > 0) {
secondary = TinkerRegistry.getShard(foundMaterial);
secondary.stackSize = leftover;
}
// build an item with this
return new ItemStack[]{output, secondary};
}
/**
* Rebuilds a tool from its raw data, material info and applied modifiers
*
* @param rootNBT The root NBT tag compound of the tool to to rebuild. The NBT will be modified, overwriting old
* data.
*/
public static void rebuildTool(NBTTagCompound rootNBT, TinkersItem tinkersItem) throws TinkerGuiException {
boolean broken = TagUtil.getToolTag(rootNBT).getBoolean(Tags.BROKEN);
// Recalculate tool base stats from material stats
NBTTagList materialTag = TagUtil.getBaseMaterialsTagList(rootNBT);
List<Material> materials = TinkerUtil.getMaterialsFromTagList(materialTag);
List<PartMaterialType> pms = tinkersItem.getRequiredComponents();
// ensure all needed Stats are present
while(materials.size() < pms.size()) {
materials.add(Material.UNKNOWN);
}
for(int i = 0; i < pms.size(); i++) {
if(!pms.get(i).isValidMaterial(materials.get(i))) {
materials.set(i, Material.UNKNOWN);
}
}
// the base stats of the tool
NBTTagCompound toolTag = tinkersItem.buildTag(materials);
TagUtil.setToolTag(rootNBT, toolTag);
// and its copy for reference
rootNBT.setTag(Tags.TOOL_DATA_ORIG, toolTag.copy());
// save the old modifiers list and clean up all tags that get set by modifiers/traits
NBTTagList modifiersTagOld = TagUtil.getModifiersTagList(rootNBT);
rootNBT.removeTag(Tags.TOOL_MODIFIERS); // the active-modifiers tag
rootNBT.setTag(Tags.TOOL_MODIFIERS, new NBTTagList());
rootNBT.removeTag("ench"); // and the enchantments tag
rootNBT.removeTag(Tags.ENCHANT_EFFECT); // enchant effect too, will be readded by a trait either way
// clean up traits
rootNBT.removeTag(Tags.TOOL_TRAITS);
tinkersItem.addMaterialTraits(rootNBT, materials);
// fire event
TinkerEvent.OnItemBuilding.fireEvent(rootNBT, ImmutableList.copyOf(materials), tinkersItem);
// reapply modifiers
NBTTagList modifiers = TagUtil.getBaseModifiersTagList(rootNBT);
NBTTagList modifiersTag = TagUtil.getModifiersTagList(rootNBT);
// copy over and reapply all relevant modifiers
for(int i = 0; i < modifiers.tagCount(); i++) {
String identifier = modifiers.getStringTagAt(i);
IModifier modifier = TinkerRegistry.getModifier(identifier);
if(modifier == null) {
log.debug("Missing modifier: {}", identifier);
continue;
}
NBTTagCompound tag;
int index = TinkerUtil.getIndexInList(modifiersTagOld, modifier.getIdentifier());
if(index >= 0) {
tag = modifiersTagOld.getCompoundTagAt(index);
}
else {
tag = new NBTTagCompound();
}
modifier.applyEffect(rootNBT, tag);
if(!tag.hasNoTags()) {
int indexNew = TinkerUtil.getIndexInList(modifiersTag, modifier.getIdentifier());
if(indexNew >= 0) {
modifiersTag.set(indexNew, tag);
}
else {
modifiersTag.appendTag(tag);
}
}
}
// remaining info, get updated toolTag
toolTag = TagUtil.getToolTag(rootNBT);
// adjust free modifiers
int freeModifiers = toolTag.getInteger(Tags.FREE_MODIFIERS);
freeModifiers -= TagUtil.getBaseModifiersUsed(rootNBT);
toolTag.setInteger(Tags.FREE_MODIFIERS, Math.max(0, freeModifiers));
// broken?
if(broken) {
toolTag.setBoolean(Tags.BROKEN, true);
}
TagUtil.setToolTag(rootNBT, toolTag);
if(freeModifiers < 0) {
throw new TinkerGuiException(Util.translateFormatted("gui.error.not_enough_modifiers", -freeModifiers));
}
}
public static short getEnchantmentLevel(NBTTagCompound rootTag, Enchantment enchantment) {
NBTTagList enchantments = rootTag.getTagList("ench", 10);
if(enchantments == null) {
enchantments = new NBTTagList();
}
int id = Enchantment.getEnchantmentID(enchantment);
for(int i = 0; i < enchantments.tagCount(); i++) {
if(enchantments.getCompoundTagAt(i).getShort("id") == id) {
return enchantments.getCompoundTagAt(i).getShort("lvl");
}
}
return 0;
}
public static void addEnchantment(NBTTagCompound rootTag, Enchantment enchantment) {
NBTTagList enchantments = rootTag.getTagList("ench", 10);
if(enchantments == null) {
enchantments = new NBTTagList();
}
NBTTagCompound enchTag = new NBTTagCompound();
int enchId = Enchantment.getEnchantmentID(enchantment);
int id = -1;
for(int i = 0; i < enchantments.tagCount(); i++) {
if(enchantments.getCompoundTagAt(i).getShort("id") == enchId) {
enchTag = enchantments.getCompoundTagAt(i);
id = i;
break;
}
}
int level = enchTag.getShort("lvl") + 1;
level = Math.min(level, enchantment.getMaxLevel());
enchTag.setShort("id", (short) enchId);
enchTag.setShort("lvl", (short) level);
if(id < 0) {
enchantments.appendTag(enchTag);
}
else {
enchantments.set(id, enchTag);
}
rootTag.setTag("ench", enchantments);
}
}