aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/gregtech/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/gregtech/api')
-rw-r--r--src/main/java/gregtech/api/metatileentity/implementations/GT_MetaTileEntity_MultiBlockBase.java24
-rw-r--r--src/main/java/gregtech/api/util/GT_AssemblyLineUtils.java440
-rw-r--r--src/main/java/gregtech/api/util/GT_LanguageManager.java2
-rw-r--r--src/main/java/gregtech/api/util/GT_Recipe.java78
-rw-r--r--src/main/java/gregtech/api/util/GT_Single_Recipe_Check.java284
-rw-r--r--src/main/java/gregtech/api/util/GT_Single_Recipe_Check_Processing_Array.java212
6 files changed, 1040 insertions, 0 deletions
diff --git a/src/main/java/gregtech/api/metatileentity/implementations/GT_MetaTileEntity_MultiBlockBase.java b/src/main/java/gregtech/api/metatileentity/implementations/GT_MetaTileEntity_MultiBlockBase.java
index 7a2f1bedac..6369c48f8e 100644
--- a/src/main/java/gregtech/api/metatileentity/implementations/GT_MetaTileEntity_MultiBlockBase.java
+++ b/src/main/java/gregtech/api/metatileentity/implementations/GT_MetaTileEntity_MultiBlockBase.java
@@ -13,6 +13,7 @@ import gregtech.api.objects.GT_ItemStack;
import gregtech.api.util.GT_Log;
import gregtech.api.util.GT_ModHandler;
import gregtech.api.util.GT_Recipe.GT_Recipe_Map;
+import gregtech.api.util.GT_Single_Recipe_Check;
import gregtech.api.util.GT_Utility;
import gregtech.common.GT_Pollution;
import gregtech.common.items.GT_MetaGenerated_Tool_01;
@@ -43,6 +44,9 @@ public abstract class GT_MetaTileEntity_MultiBlockBase extends MetaTileEntity {
public int damageFactorLow = 5;
public float damageFactorHigh = 0.6f;
+ public boolean mLockedToSingleRecipe = false;
+ public GT_Single_Recipe_Check mSingleRecipeCheck = null;
+
public ArrayList<GT_MetaTileEntity_Hatch_Input> mInputHatches = new ArrayList<>();
public ArrayList<GT_MetaTileEntity_Hatch_Output> mOutputHatches = new ArrayList<>();
public ArrayList<GT_MetaTileEntity_Hatch_InputBus> mInputBusses = new ArrayList<>();
@@ -81,6 +85,24 @@ public abstract class GT_MetaTileEntity_MultiBlockBase extends MetaTileEntity {
return aSide != getBaseMetaTileEntity().getFrontFacing();
}
+ /** Override this if you are a multi-block that has added support for single recipe locking. */
+ public boolean supportsSingleRecipeLocking() {
+ return false;
+ }
+
+ @Override
+ public void onScrewdriverRightClick(byte aSide, EntityPlayer aPlayer, float aX, float aY, float aZ) {
+ if (supportsSingleRecipeLocking()) {
+ mLockedToSingleRecipe = !mLockedToSingleRecipe;
+ if (mLockedToSingleRecipe) {
+ GT_Utility.sendChatToPlayer(aPlayer, trans("219","Single recipe locking enabled. Will lock to next recipe."));
+ } else {
+ GT_Utility.sendChatToPlayer(aPlayer, trans("220","Single recipe locking disabled."));
+ mSingleRecipeCheck = null;
+ }
+ }
+ }
+
@Override
public boolean isSimpleMachine() {
return false;
@@ -125,6 +147,7 @@ public abstract class GT_MetaTileEntity_MultiBlockBase extends MetaTileEntity {
aNBT.setInteger("mEfficiency", mEfficiency);
aNBT.setInteger("mPollution", mPollution);
aNBT.setInteger("mRuntime", mRuntime);
+ aNBT.setBoolean("mLockedToSingleRecipe", mLockedToSingleRecipe);
if (mOutputItems != null) {
aNBT.setInteger("mOutputItemsLength", mOutputItems.length);
@@ -162,6 +185,7 @@ public abstract class GT_MetaTileEntity_MultiBlockBase extends MetaTileEntity {
mEfficiency = aNBT.getInteger("mEfficiency");
mPollution = aNBT.getInteger("mPollution");
mRuntime = aNBT.getInteger("mRuntime");
+ mLockedToSingleRecipe = aNBT.getBoolean("mLockedToSingleRecipe");
int aOutputItemsLength = aNBT.getInteger("mOutputItemsLength");
if (aOutputItemsLength > 0) {
diff --git a/src/main/java/gregtech/api/util/GT_AssemblyLineUtils.java b/src/main/java/gregtech/api/util/GT_AssemblyLineUtils.java
new file mode 100644
index 0000000000..f35f1962d1
--- /dev/null
+++ b/src/main/java/gregtech/api/util/GT_AssemblyLineUtils.java
@@ -0,0 +1,440 @@
+package gregtech.api.util;
+
+import static gregtech.GT_Mod.GT_FML_LOGGER;
+
+import java.util.HashMap;
+
+import cpw.mods.fml.common.FMLCommonHandler;
+import gregtech.api.enums.GT_Values;
+import gregtech.api.enums.ItemList;
+import gregtech.api.objects.GT_ItemStack;
+import gregtech.api.util.GT_Recipe.GT_Recipe_AssemblyLine;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTTagString;
+import net.minecraftforge.fluids.FluidStack;
+import scala.actors.threadpool.Arrays;
+
+public class GT_AssemblyLineUtils {
+
+ /**
+ * A cache of Recipes using the Output as Key.
+ */
+ private static HashMap<GT_ItemStack, GT_Recipe_AssemblyLine> sRecipeCacheByOutput = new HashMap<GT_ItemStack, GT_Recipe_AssemblyLine>();
+ /**
+ * A cache of Recipes using the Recipe Hash String as Key.
+ */
+ private static HashMap<String, GT_Recipe_AssemblyLine> sRecipeCacheByRecipeHash = new HashMap<String, GT_Recipe_AssemblyLine>();
+
+
+
+ /**
+ * Checks the DataStick for deprecated/invalid recipes, updating them as required.
+ * @param aDataStick - The DataStick to process
+ * @return Is this DataStick now valid with a current recipe?
+ */
+ public static boolean processDataStick(ItemStack aDataStick) {
+ if (!isItemDataStick(aDataStick)) {
+ return false;
+ }
+ if (doesDataStickNeedUpdate(aDataStick)) {
+ ItemStack aStickOutput = getDataStickOutput(aDataStick);
+ if (aStickOutput != null) {
+ GT_Recipe_AssemblyLine aIntendedRecipe = findAssemblyLineRecipeByOutput(aStickOutput);
+ if (aIntendedRecipe != null) {
+ return setAssemblyLineRecipeOnDataStick(aDataStick, aIntendedRecipe);
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+
+ /**
+ * Finds an Assembly Line recipe from a DataStick.
+ * @param aDataStick - The DataStick to check.
+ * @return The GT_Recipe_AssemblyLine recipe contained on the DataStick, if any.
+ */
+ public static GT_Recipe_AssemblyLine findAssemblyLineRecipeFromDataStick(ItemStack aDataStick) {
+ return findAssemblyLineRecipeFromDataStick(aDataStick, false);
+ }
+
+ /**
+ * Finds an Assembly Line recipe from a DataStick.
+ * @param aDataStick - The DataStick to check.
+ * @param aReturnBuiltRecipe - Do we return a GT_Recipe_AssemblyLine built from the data on the Data Stick instead of searching the Recipe Map?
+ * @return The GT_Recipe_AssemblyLine recipe contained on the DataStick, if any.
+ */
+ public static GT_Recipe_AssemblyLine findAssemblyLineRecipeFromDataStick(ItemStack aDataStick, boolean aReturnBuiltRecipe) {
+ if (!isItemDataStick(aDataStick)) {
+ return null;
+ }
+ ItemStack[] aInputs = new ItemStack[15];
+ ItemStack[] aOutputs = new ItemStack[1];
+ FluidStack[] aFluidInputs = new FluidStack[4];
+
+ NBTTagCompound aTag = aDataStick.getTagCompound();
+ if (aTag == null) {
+ return null;
+ }
+
+ //Get From Cache
+ if (doesDataStickHaveRecipeHash(aDataStick)) {
+ GT_Recipe_AssemblyLine aRecipeFromCache = sRecipeCacheByRecipeHash.get(getHashFromDataStack(aDataStick));
+ if (aRecipeFromCache != null) {
+ return aRecipeFromCache;
+ }
+ }
+
+ for (int i = 0; i < 15; i++) {
+ int count = aTag.getInteger("a" + i);
+ if (!aTag.hasKey("" + i) && count <= 0) {
+ continue;
+ }
+
+ boolean flag = true;
+ if (count > 0) {
+ for (int j = 0; j < count; j++) {
+ aInputs[i] = GT_Utility.loadItem(aTag, "a" + i + ":" + j);
+ if (aInputs[i] == null) {
+ continue;
+ }
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info("Item " + i + " : " + aInputs[i].getUnlocalizedName());
+ }
+ flag = false;
+ break;
+ }
+ }
+ if (flag) {
+ aInputs[i] = GT_Utility.loadItem(aTag, "" + i);
+ if (aInputs[i] == null) {
+ flag = false;
+ continue;
+ }
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info("Item " + i + " : " + aInputs[i].getUnlocalizedName());
+ }
+ flag = false;
+ }
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info(i + (flag ? " not accepted" : " accepted"));
+ }
+ }
+
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info("All Items done, start fluid check");
+ }
+ for (int i = 0; i < 4; i++) {
+ if (!aTag.hasKey("f" + i)) continue;
+ aFluidInputs[i] = GT_Utility.loadFluid(aTag, "f" + i);
+ if (aFluidInputs[i] == null) continue;
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info("Fluid " + i + " " + aFluidInputs[i].getUnlocalizedName());
+ }
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info(i + " accepted");
+ }
+ }
+ aOutputs = new ItemStack[]{GT_Utility.loadItem(aTag, "output")};
+ if (!aTag.hasKey("output") || !aTag.hasKey("time") || aTag.getInteger("time") <= 0 || !aTag.hasKey("eu") || aOutputs[0] == null || !GT_Utility.isStackValid(aOutputs[0])) {
+ return null;
+ }
+ if (GT_Values.D1) {
+ GT_FML_LOGGER.info("Found Data Stick recipe");
+ }
+
+ int aTime = aTag.getInteger("time");
+ int aEU = aTag.getInteger("eu");
+
+ // Try build a recipe instance
+ if (aReturnBuiltRecipe) {
+ GT_Recipe_AssemblyLine aBuiltRecipe = new GT_Recipe_AssemblyLine(null, 0, aInputs, aFluidInputs, aOutputs[0], aTime, aEU);
+ return aBuiltRecipe;
+ }
+
+
+ for (GT_Recipe_AssemblyLine aRecipe : GT_Recipe.GT_Recipe_AssemblyLine.sAssemblylineRecipes) {
+ if (aRecipe.mEUt == aEU && aRecipe.mDuration == aTime) {
+ if (GT_Utility.areStacksEqual(aOutputs[0], aRecipe.mOutput, true)) {
+ if (Arrays.equals(aRecipe.mInputs, aInputs) && Arrays.equals(aRecipe.mFluidInputs, aFluidInputs)) {
+ // Cache it
+ String aRecipeHash = generateRecipeHash(aRecipe);
+ sRecipeCacheByRecipeHash.put(aRecipeHash, aRecipe);
+ sRecipeCacheByOutput.put(new GT_ItemStack(aRecipe.mOutput), aRecipe);
+ return aRecipe;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Finds a GT_Recipe_AssemblyLine based on the expected output ItemStack.
+ * @param aOutput - The Output of a GT_Recipe_AssemblyLine.
+ * @return First found GT_Recipe_AssemblyLine with matching output.
+ */
+ public static GT_Recipe_AssemblyLine findAssemblyLineRecipeByOutput(ItemStack aOutput) {
+ if (aOutput == null) {
+ return null;
+ }
+
+ // Check the cache
+ GT_ItemStack aCacheStack = new GT_ItemStack(aOutput);
+ GT_Recipe_AssemblyLine aRecipeFromCache = sRecipeCacheByOutput.get(aCacheStack);
+ if (aRecipeFromCache != null) {
+ return aRecipeFromCache;
+ }
+
+ // Iterate all recipes and return the first matching based on Output.
+ for (GT_Recipe_AssemblyLine aRecipe : GT_Recipe.GT_Recipe_AssemblyLine.sAssemblylineRecipes) {
+ ItemStack aRecipeOutput = aRecipe.mOutput;
+ if (GT_Utility.areStacksEqual(aRecipeOutput, aOutput)) {
+ // Cache it to prevent future iterations of all recipes
+ sRecipeCacheByOutput.put(aCacheStack, aRecipe);
+ sRecipeCacheByRecipeHash.put(generateRecipeHash(aRecipe), aRecipe);
+ return aRecipe;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param aRecipe - The recipe to generate a Recipe Hash String from.
+ * @return The Recipe Hash String.
+ */
+ public static String generateRecipeHash(GT_Recipe_AssemblyLine aRecipe) {
+ String aHash = "Invalid.Recipe.Hash";
+ if (aRecipe != null) {
+ aHash = "Hash."+aRecipe.hashCode();
+ }
+ return aHash;
+ }
+
+ /**
+ * @param aHash - Recipe hash String, may be null but will just be treated as invalid.
+ * @return Is this Recipe Hash String valid?
+ */
+ public static boolean isValidHash(String aHash) {
+ if (aHash != null && aHash.length() > 0) {
+ if (!aHash.equals("Invalid.Recipe.Hash") && aHash.contains("Hash.")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param aStack - The ItemStack to check.
+ * @return Is this ItemStack a Data Stick?
+ */
+ public static boolean isItemDataStick(ItemStack aStack) {
+ return GT_Utility.isStackValid(aStack) && ItemList.Tool_DataStick.isStackEqual(aStack, false, true);
+ }
+
+ /**
+ * @param aDataStick - The Data Stick to check.
+ * @return Does this Data Stick have a valid output ItemStack?
+ */
+ public static boolean doesDataStickHaveOutput(ItemStack aDataStick) {
+ if (isItemDataStick(aDataStick) && aDataStick.hasTagCompound() && aDataStick.getTagCompound().hasKey("output")) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param aDataStick - The Data Stick to check.
+ * @return Does this Data Stick need recipe data updated.
+ */
+ public static boolean doesDataStickNeedUpdate(ItemStack aDataStick) {
+ if (isItemDataStick(aDataStick) && doesDataStickHaveRecipeHash(aDataStick)) {
+ String aStickHash = getHashFromDataStack(aDataStick);
+ if (isValidHash(aStickHash) && doesDataStickHaveOutput(aDataStick)) {
+ ItemStack aStickOutput = getDataStickOutput(aDataStick);
+ GT_Recipe_AssemblyLine aIntendedRecipe = findAssemblyLineRecipeByOutput(aStickOutput);
+ if (aStickHash.equals(generateRecipeHash(aIntendedRecipe))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @param aDataStick - The Data Stick to check.
+ * @return Does this have a Recipe Hash String at all?
+ */
+ public static boolean doesDataStickHaveRecipeHash(ItemStack aDataStick) {
+ if (isItemDataStick(aDataStick) && aDataStick.hasTagCompound()) {
+ NBTTagCompound aNBT = aDataStick.getTagCompound();
+ if (aNBT.hasKey("Data.Recipe.Hash")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the Output ItemStack from a Data Stick.
+ * @param aDataStick - The Data Stick to check.
+ * @return Output ItemStack contained on the Data Stick.
+ */
+ public static ItemStack getDataStickOutput(ItemStack aDataStick) {
+ if (doesDataStickHaveOutput(aDataStick)) {
+ ItemStack aOutput = GT_Utility.loadItem(aDataStick.getTagCompound(), "output");
+ return aOutput;
+ }
+ return null;
+ }
+
+ /**
+ * @param aDataStick - The Data Stick to procces.
+ * @return The stored Recipe Hash String on the Data Stick, will return an invalid Hash if one is not found. <p>
+ * Check with isValidHash(). <p>
+ * Will not return Null.
+ */
+ public static String getHashFromDataStack(ItemStack aDataStick) {
+ if (isItemDataStick(aDataStick) && aDataStick.hasTagCompound()) {
+ NBTTagCompound aNBT = aDataStick.getTagCompound();
+ if (aNBT.hasKey("Data.Recipe.Hash")) {
+ return aNBT.getString("Data.Recipe.Hash");
+
+ }
+ }
+ return "Invalid.Recipe.Hash";
+ }
+
+ /**
+ *
+ * @param aDataStick - The Data Stick to update.
+ * @param aRecipeHash - The Recipe Hash String to update with.
+ * @return Did we update the Recipe Hash String on the Data Stick?
+ */
+ public static boolean setRecipeHashOnDataStick(ItemStack aDataStick, String aRecipeHash) {
+ if (isItemDataStick(aDataStick) && aDataStick.hasTagCompound()) {
+ NBTTagCompound aNBT = aDataStick.getTagCompound();
+ aNBT.setString("Data.Recipe.Hash", aRecipeHash);
+ aDataStick.setTagCompound(aNBT);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ *
+ * @param aDataStick - The Data Stick to update.
+ * @param aNewRecipe - The New GT_Recipe_AssemblyLine recipe to update it with.
+ * @return Did we set the new recipe data & Recipe Hash String on the Data Stick?
+ */
+ public static boolean setAssemblyLineRecipeOnDataStick(ItemStack aDataStick, GT_Recipe_AssemblyLine aNewRecipe) {
+ if (isItemDataStick(aDataStick)) {
+ String s = aNewRecipe.mOutput.getDisplayName();
+ if (FMLCommonHandler.instance().getEffectiveSide().isServer()) {
+ s = GT_Assemblyline_Server.lServerNames.get(aNewRecipe.mOutput.getDisplayName());
+ if (s == null) {
+ s = aNewRecipe.mOutput.getDisplayName();
+ }
+ }
+
+ String aHash = generateRecipeHash(aNewRecipe);
+ if (GT_Values.D1) {
+ GT_Recipe_AssemblyLine aOldRecipe = findAssemblyLineRecipeFromDataStick(aDataStick, true);
+ GT_FML_LOGGER.info("Updating data stick: "+aDataStick.getDisplayName()+" | Old Recipe Hash: "+generateRecipeHash(aOldRecipe)+", New Recipe Hash: "+aHash);
+ }
+
+
+ //remove possible old NBTTagCompound
+ aDataStick.setTagCompound(new NBTTagCompound());
+ if (GT_Values.D1) {
+ GT_Utility.ItemNBT.setBookTitle(aDataStick, s + " Construction Data ("+aHash+")");
+ }
+ else {
+ GT_Utility.ItemNBT.setBookTitle(aDataStick, s + " Construction Data");
+ }
+
+ NBTTagCompound tNBT = aDataStick.getTagCompound();
+ if (tNBT == null) {
+ tNBT = new NBTTagCompound();
+ }
+
+ tNBT.setTag("output", aNewRecipe.mOutput.writeToNBT(new NBTTagCompound()));
+ tNBT.setInteger("time", aNewRecipe.mDuration);
+ tNBT.setInteger("eu", aNewRecipe.mEUt);
+ for (int i = 0; i < aNewRecipe.mInputs.length; i++) {
+ tNBT.setTag("" + i, aNewRecipe.mInputs[i].writeToNBT(new NBTTagCompound()));
+ }
+ for (int i = 0; i < aNewRecipe.mOreDictAlt.length; i++) {
+ if (aNewRecipe.mOreDictAlt[i] != null && aNewRecipe.mOreDictAlt[i].length > 0) {
+ tNBT.setInteger("a" + i, aNewRecipe.mOreDictAlt[i].length);
+ for (int j = 0; j < aNewRecipe.mOreDictAlt[i].length; j++) {
+ tNBT.setTag("a" + i + ":" + j, aNewRecipe.mOreDictAlt[i][j].writeToNBT(new NBTTagCompound()));
+ }
+ }
+ }
+ for (int i = 0; i < aNewRecipe.mFluidInputs.length; i++) {
+ tNBT.setTag("f" + i, aNewRecipe.mFluidInputs[i].writeToNBT(new NBTTagCompound()));
+ }
+ tNBT.setString("author", "Assembling Line Recipe Generator");
+ NBTTagList tNBTList = new NBTTagList();
+ s = aNewRecipe.mOutput.getDisplayName();
+ if (FMLCommonHandler.instance().getEffectiveSide().isServer()) {
+ s = GT_Assemblyline_Server.lServerNames.get(aNewRecipe.mOutput.getDisplayName());
+ if (s == null)
+ s = aNewRecipe.mOutput.getDisplayName();
+ }
+ tNBTList.appendTag(new NBTTagString("Construction plan for " + aNewRecipe.mOutput.stackSize + " " + s + ". Needed EU/t: " + aNewRecipe.mEUt + " Production time: " + (aNewRecipe.mDuration / 20)));
+ for (int i = 0; i < aNewRecipe.mInputs.length; i++) {
+ if (aNewRecipe.mOreDictAlt[i] != null) {
+ int count = 0;
+ StringBuilder tBuilder = new StringBuilder("Input Bus " + (i + 1) + ": ");
+ for (ItemStack tStack : aNewRecipe.mOreDictAlt[i]) {
+ if (tStack != null) {
+ s = tStack.getDisplayName();
+ if (FMLCommonHandler.instance().getEffectiveSide().isServer()) {
+ s = GT_Assemblyline_Server.lServerNames.get(tStack.getDisplayName());
+ if (s == null)
+ s = tStack.getDisplayName();
+ }
+
+
+ tBuilder.append(count == 0 ? "" : "\nOr ").append(tStack.stackSize).append(" ").append(s);
+ count++;
+ }
+ }
+ if (count > 0) tNBTList.appendTag(new NBTTagString(tBuilder.toString()));
+ } else if (aNewRecipe.mInputs[i] != null) {
+ s = aNewRecipe.mInputs[i].getDisplayName();
+ if (FMLCommonHandler.instance().getEffectiveSide().isServer()) {
+ s = GT_Assemblyline_Server.lServerNames.get(aNewRecipe.mInputs[i].getDisplayName());
+ if (s == null)
+ s = aNewRecipe.mInputs[i].getDisplayName();
+ }
+ tNBTList.appendTag(new NBTTagString("Input Bus " + (i + 1) + ": " + aNewRecipe.mInputs[i].stackSize + " " + s));
+ }
+ }
+ for (int i = 0; i < aNewRecipe.mFluidInputs.length; i++) {
+ if (aNewRecipe.mFluidInputs[i] != null) {
+ s = aNewRecipe.mFluidInputs[i].getLocalizedName();
+ if (FMLCommonHandler.instance().getEffectiveSide().isServer()) {
+ s = GT_Assemblyline_Server.lServerNames.get(aNewRecipe.mFluidInputs[i].getLocalizedName());
+ if (s == null)
+ s = aNewRecipe.mFluidInputs[i].getLocalizedName();
+ }
+ tNBTList.appendTag(new NBTTagString("Input Hatch " + (i + 1) + ": " + aNewRecipe.mFluidInputs[i].amount + "L " + s));
+ }
+ }
+ tNBT.setTag("pages", tNBTList);
+ aDataStick.setTagCompound(tNBT);
+ // Set recipe hash
+ setRecipeHashOnDataStick(aDataStick, aHash);
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/gregtech/api/util/GT_LanguageManager.java b/src/main/java/gregtech/api/util/GT_LanguageManager.java
index 9faa7b11a6..767c830f79 100644
--- a/src/main/java/gregtech/api/util/GT_LanguageManager.java
+++ b/src/main/java/gregtech/api/util/GT_LanguageManager.java
@@ -335,6 +335,8 @@ public class GT_LanguageManager {
addStringLocalization("Interaction_DESCRIPTION_Index_216", "Deprecated Recipe");
addStringLocalization("Interaction_DESCRIPTION_Index_217", "Stocking mode. Keeps this many items in destination input slots. This mode can be server unfriendly.");
addStringLocalization("Interaction_DESCRIPTION_Index_218", "Transfer size mode. Add exactly this many items in destination input slots as long as there is room.");
+ addStringLocalization("Interaction_DESCRIPTION_Index_219", "Single recipe locking enabled. Will lock to next recipe.");
+ addStringLocalization("Interaction_DESCRIPTION_Index_220", "Single recipe locking disabled.");
addStringLocalization("Interaction_DESCRIPTION_Index_500", "Fitting: Loose - More Flow");
addStringLocalization("Interaction_DESCRIPTION_Index_501", "Fitting: Tight - More Efficiency");
diff --git a/src/main/java/gregtech/api/util/GT_Recipe.java b/src/main/java/gregtech/api/util/GT_Recipe.java
index e2f6883bb9..7b74f95a6b 100644
--- a/src/main/java/gregtech/api/util/GT_Recipe.java
+++ b/src/main/java/gregtech/api/util/GT_Recipe.java
@@ -564,6 +564,84 @@ public class GT_Recipe implements Comparable<GT_Recipe> {
mEUt = aEUt;
mOreDictAlt = aAlt;
}
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ GT_ItemStack[] thisInputs = new GT_ItemStack[this.mInputs.length];
+ int totalInputStackSize = 0;
+ for (int i=0;i<this.mInputs.length;i++) {
+ thisInputs[i] = new GT_ItemStack(this.mInputs[i]);
+ totalInputStackSize += thisInputs[i].mStackSize;
+ }
+ int inputHash = Arrays.deepHashCode(thisInputs);
+ int inputFluidHash = Arrays.deepHashCode(this.mFluidInputs);
+ GT_ItemStack thisOutput = new GT_ItemStack(mOutput);
+ GT_ItemStack thisResearch = new GT_ItemStack(mResearchItem);
+ int miscRecipeDataHash = Arrays.deepHashCode(new Object[] {
+ totalInputStackSize,
+ mDuration, mEUt,
+ thisOutput,
+ thisResearch,
+ mResearchTime
+ });
+ result = prime * result + inputFluidHash;
+ result = prime * result + inputHash;
+ result = prime * result + miscRecipeDataHash;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GT_Recipe_AssemblyLine)) {
+ return false;
+ }
+ GT_Recipe_AssemblyLine other = (GT_Recipe_AssemblyLine) obj;
+ if (this.mInputs.length != other.mInputs.length) {
+ return false;
+ }
+ if (this.mFluidInputs.length != other.mFluidInputs.length) {
+ return false;
+ }
+ // Check Outputs Match
+ GT_ItemStack output1 = new GT_ItemStack(this.mOutput);
+ GT_ItemStack output2 = new GT_ItemStack(other.mOutput);
+ if (!output1.equals(output2)) {
+ return false;
+ }
+ // Check Scanned Item Match
+ GT_ItemStack scan1 = new GT_ItemStack(this.mResearchItem);
+ GT_ItemStack scan2 = new GT_ItemStack(other.mResearchItem);
+ if (!scan1.equals(scan2)) {
+ return false;
+ }
+ // Check Items Match
+ GT_ItemStack[] thisInputs = new GT_ItemStack[this.mInputs.length];
+ GT_ItemStack[] otherInputs = new GT_ItemStack[other.mInputs.length];
+ for (int i=0;i<thisInputs.length;i++) {
+ thisInputs[i] = new GT_ItemStack(this.mInputs[i]);
+ otherInputs[i] = new GT_ItemStack(other.mInputs[i]);
+ }
+ for (int i=0;i<thisInputs.length;i++) {
+ if (!thisInputs[i].equals(otherInputs[i]) || thisInputs[i].mStackSize != otherInputs[i].mStackSize) {
+ return false;
+ }
+ }
+ // Check Fluids Match
+ for (int i=0;i<this.mFluidInputs.length;i++) {
+ if (!this.mFluidInputs[i].isFluidStackIdentical(other.mFluidInputs[i])) {
+ return false;
+ }
+ }
+
+ return this.mDuration == other.mDuration
+ && this.mEUt == other.mEUt
+ && this.mResearchTime == other.mResearchTime;
+ }
}
diff --git a/src/main/java/gregtech/api/util/GT_Single_Recipe_Check.java b/src/main/java/gregtech/api/util/GT_Single_Recipe_Check.java
new file mode 100644
index 0000000000..469f2d64a1
--- /dev/null
+++ b/src/main/java/gregtech/api/util/GT_Single_Recipe_Check.java
@@ -0,0 +1,284 @@
+package gregtech.api.util;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_MultiBlockBase;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Used by machines that are locked to a single recipe, for fast computation. */
+public class GT_Single_Recipe_Check {
+ protected final GT_MetaTileEntity_MultiBlockBase multiBlockBase;
+ protected final GT_Recipe recipe;
+ protected final ImmutableMap<ItemId, Integer> itemCost;
+ protected final ImmutableMap<Fluid, Integer> fluidCost;
+
+ protected final int totalItemCost;
+ protected final int totalFluidCost;
+
+ protected GT_Single_Recipe_Check(
+ GT_MetaTileEntity_MultiBlockBase multiBlockBase,
+ GT_Recipe recipe,
+ ImmutableMap<ItemId, Integer> itemCost,
+ ImmutableMap<Fluid, Integer> fluidCost) {
+ this.multiBlockBase = multiBlockBase;
+ this.recipe = recipe;
+ this.itemCost = itemCost;
+ this.fluidCost = fluidCost;
+
+ this.totalItemCost = itemCost.values().stream().mapToInt(Integer::intValue).sum();
+ this.totalFluidCost = fluidCost.values().stream().mapToInt(Integer::intValue).sum();
+ }
+
+ public GT_Recipe getRecipe() {
+ return recipe;
+ }
+
+ /**
+ * Use this method if recipes cannot require more than a single stack of any item or fluid.
+ * In particular, {@link GT_MetaTileEntity_MultiBlockBase#getCompactedInputs()} and
+ * {@link GT_MetaTileEntity_MultiBlockBase#getCompactedFluids()} both enforce this single-stack
+ * restriction, so any multi-block that calls those can use this method.
+ */
+ public boolean checkRecipeInputsSingleStack(boolean consumeInputs) {
+ Map<ItemId, ItemStack> itemMap = null;
+ if (totalItemCost > 0) {
+ itemMap = new HashMap<>();
+ for (ItemStack itemStack : multiBlockBase.getStoredInputs()) {
+ itemMap.merge(
+ ItemId.createNoCopy(itemStack), itemStack,
+ (a, b) -> a.stackSize >= b.stackSize ? a : b);
+ }
+
+ for (Map.Entry<ItemId, Integer> entry : itemCost.entrySet()) {
+ ItemStack itemStack = itemMap.get(entry.getKey());
+ if (itemStack == null || itemStack.stackSize < entry.getValue()) {
+ return false;
+ }
+ }
+ }
+
+ Map<Fluid, FluidStack> fluidMap = null;
+ if (totalFluidCost > 0) {
+ fluidMap = new HashMap<>();
+ for (FluidStack fluidStack : multiBlockBase.getStoredFluids()) {
+ fluidMap.merge(
+ fluidStack.getFluid(), fluidStack,
+ (a, b) -> a.amount >= b.amount ? a : b);
+ }
+
+ for (Map.Entry<Fluid, Integer> entry : fluidCost.entrySet()) {
+ FluidStack fluidStack = fluidMap.get(entry.getKey());
+ if (fluidStack == null || fluidStack.amount < entry.getValue()) {
+ return false;
+ }
+ }
+ }
+
+ if (consumeInputs) {
+ if (totalItemCost > 0) {
+ for (Map.Entry<ItemId, Integer> entry : itemCost.entrySet()) {
+ itemMap.get(entry.getKey()).stackSize -= entry.getValue();
+ }
+ }
+
+ if (totalFluidCost > 0) {
+ for (Map.Entry<Fluid, Integer> entry : fluidCost.entrySet()) {
+ fluidMap.get(entry.getKey()).amount -= entry.getValue();
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Use this method if recipes can require more than a single stack of any item or fluid.
+ * It is less efficient, though.
+ */
+ public boolean checkRecipeInputs(boolean consumeInputs) {
+ List<ItemStack> items = null;
+ if (totalItemCost > 0) {
+ items = multiBlockBase.getStoredInputs();
+
+ Map<ItemId, Integer> itemMap = new HashMap<>();
+ for (ItemStack itemStack : items) {
+ itemMap.merge(ItemId.createNoCopy(itemStack), itemStack.stackSize, Integer::sum);
+ }
+
+ for (Map.Entry<ItemId, Integer> entry : itemCost.entrySet()) {
+ if (itemMap.getOrDefault(entry.getKey(), 0) < entry.getValue()) {
+ return false;
+ }
+ }
+ }
+
+ List<FluidStack> fluids = null;
+ if (totalFluidCost > 0) {
+ fluids = multiBlockBase.getStoredFluids();
+
+ Map<Fluid, Integer> fluidMap = new HashMap<>();
+ for (FluidStack fluidStack : fluids) {
+ fluidMap.merge(fluidStack.getFluid(), fluidStack.amount, Integer::sum);
+ }
+
+ for (Map.Entry<Fluid, Integer> entry : fluidCost.entrySet()) {
+ if (fluidMap.getOrDefault(entry.getKey(), 0) < entry.getValue()) {
+ return false;
+ }
+ }
+ }
+
+ if (consumeInputs) {
+ if (totalItemCost > 0) {
+ int remainingItemCost = totalItemCost;
+ Map<ItemId, Integer> runningItemCost = Maps.newHashMap(itemCost);
+ for (ItemStack itemStack : items) {
+ ItemId key = ItemId.createNoCopy(itemStack);
+ int runningCost = runningItemCost.getOrDefault(key, 0);
+ int paid = Math.min(itemStack.stackSize, runningCost);
+ itemStack.stackSize -= paid;
+ runningItemCost.put(key, runningCost - paid);
+
+ remainingItemCost -= paid;
+ if (remainingItemCost <= 0) {
+ break;
+ }
+ }
+ }
+
+ if (totalFluidCost > 0) {
+ int remainingFluidCost = totalFluidCost;
+ Map<Fluid, Integer> runningFluidCost = Maps.newHashMap(fluidCost);
+ for (FluidStack fluidStack : fluids) {
+ Fluid key = fluidStack.getFluid();
+ int runningCost = runningFluidCost.getOrDefault(key, 0);
+ int paid = Math.min(fluidStack.amount, runningCost);
+ fluidStack.amount -= paid;
+ runningFluidCost.put(key, runningCost - paid);
+
+ remainingFluidCost -= paid;
+ if (remainingFluidCost <= 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ protected static Map<ItemId, Integer> buildItemMap(
+ GT_MetaTileEntity_MultiBlockBase multiBlockBase) {
+ Map<ItemId, Integer> itemMap = new HashMap<>();
+ for (ItemStack itemStack : multiBlockBase.getStoredInputs()) {
+ itemMap.merge(ItemId.create(itemStack), itemStack.stackSize, Integer::sum);
+ }
+ return itemMap;
+ }
+
+ protected static Map<Fluid, Integer> buildFluidMap(
+ GT_MetaTileEntity_MultiBlockBase multiBlockBase) {
+ Map<Fluid, Integer> fluidMap = new HashMap<>();
+ for (FluidStack fluidStack : multiBlockBase.getStoredFluids()) {
+ fluidMap.merge(fluidStack.getFluid(), fluidStack.amount, Integer::sum);
+ }
+ return fluidMap;
+ }
+
+ public static Builder builder(GT_MetaTileEntity_MultiBlockBase multiBlockBase) {
+ return new Builder(multiBlockBase);
+ }
+
+ public static final class Builder {
+ private final GT_MetaTileEntity_MultiBlockBase multiBlockBase;
+
+ // In order to compute which items and fluids are consumed by the recipe, we compare the
+ // multi-block's items and fluids before and after inputs are consumed by the recipe.
+ private Map<ItemId, Integer> beforeItems;
+ private Map<Fluid, Integer> beforeFluids;
+ private Map<ItemId, Integer> afterItems;
+ private Map<Fluid, Integer> afterFluids;
+
+ private GT_Recipe recipe;
+
+ private Builder(GT_MetaTileEntity_MultiBlockBase multiBlockBase) {
+ this.multiBlockBase = multiBlockBase;
+ }
+
+ /** Call this before inputs are consumed by the recipe. */
+ public Builder setBefore() {
+ beforeItems = buildItemMap(multiBlockBase);
+ beforeFluids = buildFluidMap(multiBlockBase);
+ return this;
+ }
+
+ /** Call this after inputs are consumed by the recipe. */
+ public Builder setAfter() {
+ afterItems = buildItemMap(multiBlockBase);
+ afterFluids = buildFluidMap(multiBlockBase);
+ return this;
+ }
+
+ public Builder setRecipe(GT_Recipe recipe) {
+ this.recipe = recipe;
+ return this;
+ }
+
+ public GT_Single_Recipe_Check build() {
+ ImmutableMap.Builder<ItemId, Integer> itemCostBuilder = ImmutableMap.builder();
+ for (Map.Entry<ItemId, Integer> entry : beforeItems.entrySet()) {
+ int cost = entry.getValue() - afterItems.getOrDefault(entry.getKey(), 0);
+ if (cost > 0) {
+ itemCostBuilder.put(entry.getKey(), cost);
+ }
+ }
+
+ ImmutableMap.Builder<Fluid, Integer> fluidCostBuilder = ImmutableMap.builder();
+ for (Map.Entry<Fluid, Integer> entry : beforeFluids.entrySet()) {
+ int cost = entry.getValue() - afterFluids.getOrDefault(entry.getKey(), 0);
+ if (cost > 0) {
+ fluidCostBuilder.put(entry.getKey(), cost);
+ }
+ }
+
+ return new GT_Single_Recipe_Check(
+ multiBlockBase, recipe, itemCostBuilder.build(), fluidCostBuilder.build());
+ }
+ }
+
+ @AutoValue
+ protected abstract static class ItemId {
+ /** This method copies NBT, as it is mutable. */
+ protected static ItemId create(ItemStack itemStack) {
+ NBTTagCompound nbt = itemStack.getTagCompound();
+ if (nbt != null) {
+ nbt = (NBTTagCompound) nbt.copy();
+ }
+
+ return new AutoValue_GT_Single_Recipe_Check_ItemId(
+ itemStack.getItem(), itemStack.getItemDamage(), nbt);
+ }
+
+ /** This method does not copy NBT in order to save time. Make sure not to mutate it! */
+ protected static ItemId createNoCopy(ItemStack itemStack) {
+ return new AutoValue_GT_Single_Recipe_Check_ItemId(
+ itemStack.getItem(), itemStack.getItemDamage(), itemStack.getTagCompound());
+ }
+
+ protected abstract Item item();
+ protected abstract int metaData();
+
+ @Nullable
+ protected abstract NBTTagCompound nbt();
+ }
+}
diff --git a/src/main/java/gregtech/api/util/GT_Single_Recipe_Check_Processing_Array.java b/src/main/java/gregtech/api/util/GT_Single_Recipe_Check_Processing_Array.java
new file mode 100644
index 0000000000..225e0bd723
--- /dev/null
+++ b/src/main/java/gregtech/api/util/GT_Single_Recipe_Check_Processing_Array.java
@@ -0,0 +1,212 @@
+package gregtech.api.util;
+
+import com.google.common.collect.ImmutableMap;
+import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_MultiBlockBase;
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.fluids.Fluid;
+import net.minecraftforge.fluids.FluidStack;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Processing Array-specialized version of {@link gregtech.api.util.GT_Single_Recipe_Check}. */
+public class GT_Single_Recipe_Check_Processing_Array extends GT_Single_Recipe_Check {
+ protected final int recipeAmperage;
+
+ /**
+ * The machine type that the locked recipe is for.
+ * We will not allow running with other machines.
+ */
+ protected final ItemStack machineStack;
+
+ protected GT_Single_Recipe_Check_Processing_Array(
+ GT_MetaTileEntity_MultiBlockBase multiBlockBase,
+ GT_Recipe recipe,
+ ImmutableMap<ItemId, Integer> itemCost,
+ ImmutableMap<Fluid, Integer> fluidCost,
+ int recipeAmperage,
+ ItemStack machineStack) {
+ super(multiBlockBase, recipe, itemCost, fluidCost);
+
+ this.recipeAmperage = recipeAmperage;
+ this.machineStack = machineStack;
+ }
+
+ public int getRecipeAmperage() {
+ return recipeAmperage;
+ }
+
+ @Override
+ public boolean checkRecipeInputsSingleStack(boolean consumeInputs) {
+ throw new UnsupportedOperationException("Use checkRecipeInputs(boolean, int) instead.");
+ }
+
+ @Override
+ public boolean checkRecipeInputs(boolean consumeInputs) {
+ throw new UnsupportedOperationException("Use checkRecipeInputs(boolean, int) instead.");
+ }
+
+ /** Returns the number of parallel recipes, or 0 if recipe is not satisfied at all. */
+ public int checkRecipeInputs(boolean consumeInputs, int maxParallel) {
+ if (!GT_Utility.areStacksEqual(machineStack, multiBlockBase.mInventory[1])) {
+ // Machine stack was modified. This is not allowed, so stop processing.
+ return 0;
+ }
+ int parallel = maxParallel;
+
+ List<ItemStack> items = null;
+ if (totalItemCost > 0) {
+ items = multiBlockBase.getStoredInputs();
+
+ Map<ItemId, Integer> itemMap = new HashMap<>();
+ for (ItemStack itemStack : items) {
+ itemMap.merge(ItemId.createNoCopy(itemStack), itemStack.stackSize, Integer::sum);
+ }
+
+ for (Map.Entry<ItemId, Integer> entry : itemCost.entrySet()) {
+ parallel = Math.min(parallel, itemMap.getOrDefault(entry.getKey(), 0) / entry.getValue());
+ if (parallel <= 0) {
+ return 0;
+ }
+ }
+ }
+
+ List<FluidStack> fluids = null;
+ if (totalFluidCost > 0) {
+ fluids = multiBlockBase.getStoredFluids();
+
+ Map<Fluid, Integer> fluidMap = new HashMap<>();
+ for (FluidStack fluidStack : fluids) {
+ fluidMap.merge(fluidStack.getFluid(), fluidStack.amount, Integer::sum);
+ }
+
+ for (Map.Entry<Fluid, Integer> entry : fluidCost.entrySet()) {
+ parallel = Math.min(parallel, fluidMap.getOrDefault(entry.getKey(), 0) / entry.getValue());
+ if (parallel <= 0) {
+ return 0;
+ }
+ }
+ }
+
+ final int finalParallel = parallel;
+ if (consumeInputs) {
+ if (totalItemCost > 0) {
+ int remainingItemCost = totalItemCost * finalParallel;
+ Map<ItemId, Integer> runningItemCost =
+ itemCost.entrySet().stream()
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> entry.getValue() * finalParallel));
+
+ for (ItemStack itemStack : items) {
+ ItemId key = ItemId.createNoCopy(itemStack);
+ int runningCost = runningItemCost.getOrDefault(key, 0);
+ int paid = Math.min(itemStack.stackSize, runningCost);
+ itemStack.stackSize -= paid;
+ runningItemCost.put(key, runningCost - paid);
+
+ remainingItemCost -= paid;
+ if (remainingItemCost <= 0) {
+ break;
+ }
+ }
+ }
+
+ if (totalFluidCost > 0) {
+ int remainingFluidCost = totalFluidCost * finalParallel;
+ Map<Fluid, Integer> runningFluidCost =
+ fluidCost.entrySet().stream()
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> entry.getValue() * finalParallel));
+
+ for (FluidStack fluidStack : fluids) {
+ Fluid key = fluidStack.getFluid();
+ int runningCost = runningFluidCost.getOrDefault(key, 0);
+ int paid = Math.min(fluidStack.amount, runningCost);
+ fluidStack.amount -= paid;
+ runningFluidCost.put(key, runningCost - paid);
+
+ remainingFluidCost -= paid;
+ if (remainingFluidCost <= 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ return finalParallel;
+ }
+
+ public static Builder processingArrayBuilder(GT_MetaTileEntity_MultiBlockBase multiBlockBase) {
+ return new Builder(multiBlockBase);
+ }
+
+ public static final class Builder {
+ private final GT_MetaTileEntity_MultiBlockBase multiBlockBase;
+
+ // In order to compute which items and fluids are consumed by the recipe, we compare the
+ // multi-block's items and fluids before and after inputs are consumed by the recipe.
+ private Map<ItemId, Integer> beforeItems;
+ private Map<Fluid, Integer> beforeFluids;
+ private Map<ItemId, Integer> afterItems;
+ private Map<Fluid, Integer> afterFluids;
+
+ private GT_Recipe recipe;
+ private int recipeAmperage;
+
+ private Builder(GT_MetaTileEntity_MultiBlockBase multiBlockBase) {
+ this.multiBlockBase = multiBlockBase;
+ }
+
+ /** Call this before inputs are consumed by the recipe. */
+ public Builder setBefore() {
+ beforeItems = buildItemMap(multiBlockBase);
+ beforeFluids = buildFluidMap(multiBlockBase);
+ return this;
+ }
+
+ /** Call this after inputs are consumed by the recipe. */
+ public Builder setAfter() {
+ afterItems = buildItemMap(multiBlockBase);
+ afterFluids = buildFluidMap(multiBlockBase);
+ return this;
+ }
+
+ public Builder setRecipe(GT_Recipe recipe) {
+ this.recipe = recipe;
+ return this;
+ }
+
+ public Builder setRecipeAmperage(int recipeAmperage) {
+ this.recipeAmperage = recipeAmperage;
+ return this;
+ }
+
+ public GT_Single_Recipe_Check_Processing_Array build() {
+ ImmutableMap.Builder<ItemId, Integer> itemCostBuilder = ImmutableMap.builder();
+ for (Map.Entry<ItemId, Integer> entry : beforeItems.entrySet()) {
+ int cost = entry.getValue() - afterItems.getOrDefault(entry.getKey(), 0);
+ if (cost > 0) {
+ itemCostBuilder.put(entry.getKey(), cost);
+ }
+ }
+
+ ImmutableMap.Builder<Fluid, Integer> fluidCostBuilder = ImmutableMap.builder();
+ for (Map.Entry<Fluid, Integer> entry : beforeFluids.entrySet()) {
+ int cost = entry.getValue() - afterFluids.getOrDefault(entry.getKey(), 0);
+ if (cost > 0) {
+ fluidCostBuilder.put(entry.getKey(), cost);
+ }
+ }
+
+ return new GT_Single_Recipe_Check_Processing_Array(
+ multiBlockBase, recipe, itemCostBuilder.build(), fluidCostBuilder.build(),
+ recipeAmperage, multiBlockBase.mInventory[1].copy());
+ }
+ }
+} \ No newline at end of file