diff --git a/src/main/java/electroblob/wizardry/Settings.java b/src/main/java/electroblob/wizardry/Settings.java
index d58ff358..952d9062 100644
--- a/src/main/java/electroblob/wizardry/Settings.java
+++ b/src/main/java/electroblob/wizardry/Settings.java
@@ -241,10 +241,11 @@ public final class Settings {
public String[] damageSourceBlacklist = {};
/** [Server-only] Whether to print compatibility warnings to the console. */
public boolean compatibilityWarnings = true;
+ // TODO: Should these really be server-only?
/** [Server-only] Whether Baubles integration features are enabled. */
public boolean baublesIntegration = true;
-// /** [Server-only] Whether JEI integration features are enabled. */
-// public boolean jeiIntegration = true;
+ /** [Server-only] Whether JEI integration features are enabled. */
+ public boolean jeiIntegration = true;
/** [Server-only] Whether Antique Atlas integration features are enabled. */
public boolean antiqueAtlasIntegration = true;
/** [Server-only] Whether global markers for wizard towers are added to antique atlases. */
@@ -1111,13 +1112,13 @@ public final class Settings {
baublesIntegration = property.getBoolean();
propOrder.add(property.getName());
-// property = config.get(COMPATIBILITY_CATEGORY, "jeiIntegration", true,
-// "If JEI (Just Enough Items) is installed, controls whether JEI integration features are enabled. If this is disabled, wizardry will always behave as if JEI is not installed.");
-// property.setLanguageKey("config." + Wizardry.MODID + ".jei_integration");
-// property.setRequiresMcRestart(true);
-// Wizardry.proxy.setToNamedBooleanEntry(property);
-// jeiIntegration = property.getBoolean();
-// propOrder.add(property.getName());
+ property = config.get(COMPATIBILITY_CATEGORY, "jeiIntegration", true,
+ "If JEI (Just Enough Items) is installed, controls whether JEI integration features are enabled. If this is disabled, wizardry will always behave as if JEI is not installed.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".jei_integration");
+ property.setRequiresMcRestart(true);
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ jeiIntegration = property.getBoolean();
+ propOrder.add(property.getName());
property = config.get(COMPATIBILITY_CATEGORY, "antiqueAtlasIntegration", true,
"If Antique Atlas is installed, controls whether Antique Atlas integration features are enabled. If this is disabled, wizardry will always behave as if Antique Atlas is not installed.");
diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java
index afc9ebe4..8b0ea1e8 100644
--- a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java
@@ -113,7 +113,7 @@ public class GuiArcaneWorkbench extends GuiContainer {
public void initGui(){
this.mc.player.openContainer = this.inventorySlots;
- this.guiLeft = (this.width - this.xSize) / 2;
+ this.guiLeft = (this.width - MAIN_GUI_WIDTH) / 2; // Use MAIN_GUI_WIDTH, not xSize, otherwise JEI messes with it
this.guiTop = (this.height - this.ySize) / 2;
Keyboard.enableRepeatEvents(true);
@@ -132,6 +132,7 @@ public class GuiArcaneWorkbench extends GuiContainer {
this.searchField.setCanLoseFocus(false);
this.searchField.setFocused(true);
+ this.tooltipElements.clear();
this.tooltipElements.add(new TooltipElementItemName(new Style().setColor(TextFormatting.WHITE), LINE_SPACING_WIDE));
this.tooltipElements.add(new TooltipElementManaReadout(LINE_SPACING_WIDE));
this.tooltipElements.add(new TooltipElementProgressionBar(LINE_SPACING_WIDE));
diff --git a/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java
new file mode 100644
index 00000000..9adea78d
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipe.java
@@ -0,0 +1,119 @@
+package electroblob.wizardry.integration.jei;
+
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.constants.Constants;
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.inventory.ContainerArcaneWorkbench;
+import electroblob.wizardry.item.IWorkbenchItem;
+import electroblob.wizardry.registry.WizardryItems;
+import mezz.jei.api.ingredients.IIngredients;
+import mezz.jei.api.ingredients.VanillaTypes;
+import mezz.jei.api.recipe.IRecipeWrapper;
+import net.minecraft.client.Minecraft;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a 'recipe' for the arcane workbench.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.3
+ */
+public class ArcaneWorkbenchRecipe implements IRecipeWrapper {
+
+ private final ItemStack centreStack;
+ private final List books;
+ private final List crystals;
+ private final List upgrades;
+ private final ItemStack result;
+
+ private final int bookSlots;
+
+ private final List> inputs;
+
+ public ArcaneWorkbenchRecipe(ItemStack centreStack, List books, List crystals, List upgrades, ItemStack result){
+
+ this.centreStack = centreStack;
+ this.books = books; // CAUTION! This list is an OUTER LIST!
+ this.crystals = crystals;
+ this.upgrades = upgrades;
+ this.result = result;
+
+ this.inputs = new ArrayList<>();
+ for(ItemStack book : books) this.inputs.add(Collections.singletonList(book));
+ this.inputs.add(crystals);
+ this.inputs.add(Collections.singletonList(centreStack));
+ this.inputs.add(upgrades);
+
+ if(centreStack.getItem() instanceof IWorkbenchItem){
+ bookSlots = ((IWorkbenchItem)centreStack.getItem()).getSpellSlotCount(centreStack);
+ }else{
+ bookSlots = 0;
+ }
+
+ }
+
+ public ArcaneWorkbenchRecipe(ItemStack centreStack, List books, int mana, List upgrades, ItemStack result){
+ this(centreStack, books, generateCrystalStacks(mana), upgrades, result);
+ }
+
+ @Override
+ public void getIngredients(IIngredients ingredients){
+ ingredients.setInputLists(VanillaTypes.ITEM, inputs);
+ ingredients.setOutput(VanillaTypes.ITEM, result);
+ }
+
+ @Override
+ public void drawInfo(Minecraft minecraft, int recipeWidth, int recipeHeight, int mouseX, int mouseY){
+ // Can't do this in IRecipeCategory#drawExtras because we have no access to the recipe there!
+ // ArcaneWorkbenchRecipeCategory.TEXTURE is already bound at this point
+ for(int i = 0; i < bookSlots; i++){
+ int x = ArcaneWorkbenchRecipeCategory.CENTRE_SLOT_X + ContainerArcaneWorkbench.getBookSlotXOffset(i, bookSlots) - 9;
+ int y = ArcaneWorkbenchRecipeCategory.CENTRE_SLOT_Y + ContainerArcaneWorkbench.getBookSlotYOffset(i, bookSlots) - 9;
+ DrawingUtils.drawTexturedRect(x, y, 0, ArcaneWorkbenchRecipeCategory.HEIGHT, 36, 36, 256, 256);
+ }
+ }
+
+ /**
+ * Returns a list of item stacks, one for each type of crystal (regular, elemental, grand and shards), each with
+ * the minimum quantity needed to supply the given amount of mana. Types of crystal for which more than the max.
+ * stack size would be needed are ignored.
+ */
+ public static List generateCrystalStacks(int mana){
+
+ if(mana < 0) throw new IllegalArgumentException("Cannot create an arcane workbench recipe with negative mana!");
+
+ if(mana == 0) return Collections.emptyList();
+
+ List crystalStacks = new ArrayList<>();
+
+ int count = MathHelper.ceil((float)mana / Constants.MANA_PER_CRYSTAL);
+ // A stack of crystals will almost certainly be enough mana, but you never know!
+ // Using ItemStack.EMPTY to avoid deprecated method; crystals' stack size is not stack-sensitive so it doesn't matter
+ if(count <= WizardryItems.magic_crystal.getItemStackLimit(ItemStack.EMPTY)){
+ for(int meta = 0; meta < Element.values().length; meta++){
+ crystalStacks.add(new ItemStack(WizardryItems.magic_crystal, count, meta));
+ }
+ }
+
+ count = MathHelper.ceil((float)mana / Constants.MANA_PER_SHARD);
+
+ if(count <= WizardryItems.crystal_shard.getItemStackLimit(ItemStack.EMPTY)){
+ crystalStacks.add(new ItemStack(WizardryItems.crystal_shard, count));
+ }
+
+ count = MathHelper.ceil((float)mana / Constants.GRAND_CRYSTAL_MANA);
+
+ if(count <= WizardryItems.grand_crystal.getItemStackLimit(ItemStack.EMPTY)){
+ crystalStacks.add(new ItemStack(WizardryItems.grand_crystal, count));
+ }
+
+ return crystalStacks;
+
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java
new file mode 100644
index 00000000..ad5896b8
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchRecipeCategory.java
@@ -0,0 +1,269 @@
+package electroblob.wizardry.integration.jei;
+
+import com.google.common.collect.Streams;
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.constants.Constants;
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.inventory.ContainerArcaneWorkbench;
+import electroblob.wizardry.item.*;
+import electroblob.wizardry.registry.WizardryItems;
+import electroblob.wizardry.registry.WizardryRecipes;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.WandHelper;
+import mezz.jei.api.IGuiHelper;
+import mezz.jei.api.gui.IDrawable;
+import mezz.jei.api.gui.IGuiItemStackGroup;
+import mezz.jei.api.gui.IRecipeLayout;
+import mezz.jei.api.ingredients.IIngredients;
+import mezz.jei.api.ingredients.VanillaTypes;
+import mezz.jei.api.recipe.IRecipeCategory;
+import mezz.jei.api.recipe.IRecipeCategoryRegistration;
+import net.minecraft.client.resources.I18n;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.NonNullList;
+import net.minecraft.util.ResourceLocation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * JEI recipe category implementation for all 'recipes' in the arcane workbench (of course, the arcane workbench
+ * doesn't have 'recipes' in the normal sense, but that's how they're displayed in JEI).
+ *
+ * @author Electroblob
+ * @since Wizardry 4.3
+ */
+public class ArcaneWorkbenchRecipeCategory implements IRecipeCategory {
+
+ static final String UID = "ebwizardry:arcane_workbench";
+ static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/gui/container/arcane_workbench_jei_background.png");
+
+ static final int WIDTH = 166;
+ static final int HEIGHT = 126;
+ // Annoyingly, these seem to be for 18x18 slots rather than the actual highlighted area, unlike container slots
+ static final int CENTRE_SLOT_X = 74;
+ static final int CENTRE_SLOT_Y = 54;
+ static final int CRYSTAL_SLOT_X = 7;
+ static final int CRYSTAL_SLOT_Y = 91;
+ static final int UPGRADE_SLOT_X = 141;
+ static final int UPGRADE_SLOT_Y = 7;
+ static final int OUTPUT_SLOT_X = 141;
+ static final int OUTPUT_SLOT_Y = 101;
+
+ private final IDrawable background;
+
+ public ArcaneWorkbenchRecipeCategory(IRecipeCategoryRegistration registry){
+ IGuiHelper helper = registry.getJeiHelpers().getGuiHelper();
+ background = helper.createDrawable(TEXTURE, 0, 0, WIDTH, HEIGHT);
+ }
+
+ @Override
+ public String getUid(){
+ return UID;
+ }
+
+ @Override
+ public String getTitle(){
+ // JEI is client-side, so client classes can safely be used here
+ return I18n.format("integration.jei.category." + UID);
+ }
+
+ @Override
+ public String getModName(){
+ return Wizardry.NAME;
+ }
+
+ @Override
+ public IDrawable getBackground(){
+ return background;
+ }
+
+ @Override
+ public void setRecipe(IRecipeLayout recipeLayout, ArcaneWorkbenchRecipe recipeWrapper, IIngredients ingredients){
+
+ // Okay, they're not technically *slots* but to all intents and purposes, that's how they behave
+ IGuiItemStackGroup slots = recipeLayout.getItemStacks();
+
+ List> inputs = ingredients.getInputs(VanillaTypes.ITEM);
+ List> outputs = ingredients.getOutputs(VanillaTypes.ITEM);
+
+ ItemStack centreStack = inputs.get(inputs.size() - 2).get(0);
+
+ int bookSlots = 0;
+
+ if(centreStack.getItem() instanceof IWorkbenchItem){
+ bookSlots = ((IWorkbenchItem)centreStack.getItem()).getSpellSlotCount(centreStack);
+ }
+
+ // Slot initialisation
+ int i = 0;
+
+ while(i < bookSlots){
+ int x = CENTRE_SLOT_X + ContainerArcaneWorkbench.getBookSlotXOffset(i, bookSlots);
+ int y = CENTRE_SLOT_Y + ContainerArcaneWorkbench.getBookSlotYOffset(i, bookSlots);
+ slots.init(i++, true, x, y);
+ }
+
+ // Add dummy slots for the hidden book slots so the transfer handler works correctly
+ // Sure, we COULD use an IRecipeTransferHandler, but this is far less effort!
+ while(i < ContainerArcaneWorkbench.CRYSTAL_SLOT){
+ slots.init(i++, true, 0, 0);
+ }
+
+ slots.init(i++, true, CRYSTAL_SLOT_X, CRYSTAL_SLOT_Y);
+ slots.init(i++, true, CENTRE_SLOT_X, CENTRE_SLOT_Y);
+ slots.init(i++, true, UPGRADE_SLOT_X, UPGRADE_SLOT_Y);
+
+ slots.init(i++, false, OUTPUT_SLOT_X, OUTPUT_SLOT_Y);
+
+ // Assign ingredients to slots
+ // The number of books we actually have is inputs.size() - 3, probably less than the number of book slots
+ for(int j = 0; j < Math.min(bookSlots, inputs.size() - 3); j++){
+ slots.set(j, inputs.get(j));
+ }
+
+ slots.set(ContainerArcaneWorkbench.CRYSTAL_SLOT, inputs.get(inputs.size() - 3));
+ slots.set(ContainerArcaneWorkbench.CENTRE_SLOT, inputs.get(inputs.size() - 2));
+ slots.set(ContainerArcaneWorkbench.UPGRADE_SLOT, inputs.get(inputs.size() - 1));
+
+ for(int k = 0; k < outputs.size(); k++) slots.set(11 + k, outputs.get(k));
+
+ }
+
+ /** Called to generate all of wizardry's arcane workbench 'recipes' for JEI. */
+ public static Collection generateRecipes(){
+
+ List recipes = new ArrayList<>();
+
+ recipes.addAll(generateUpgradeRecipes()); // Probably nicest to have these first, they're the most useful
+ recipes.addAll(generateChargingRecipes());
+ recipes.addAll(generateScrollRecipes());
+
+ return recipes;
+
+ }
+
+ private static Collection generateUpgradeRecipes(){
+
+ List recipes = new ArrayList<>();
+
+ List upgrades = new ArrayList<>();
+
+ for(Item item : Item.REGISTRY){
+ if(item instanceof ItemArcaneTome || item instanceof ItemArmourUpgrade){
+ NonNullList variants = NonNullList.create();
+ item.getSubItems(item.getCreativeTab(), variants);
+ upgrades.addAll(variants);
+ }
+ }
+
+ // Condense all special upgrades into one ingredient in an effort to reduce the number of separate recipes
+ List specialUpgrades = new ArrayList<>();
+
+ for(Item item : WandHelper.getSpecialUpgrades()){
+ NonNullList variants = NonNullList.create();
+ item.getSubItems(item.getCreativeTab(), variants);
+ specialUpgrades.addAll(variants);
+ }
+
+ for(Item item : Item.REGISTRY){
+
+ if(item instanceof IWorkbenchItem){
+
+ ItemStack original = new ItemStack(item);
+
+ for(ItemStack upgrade : upgrades){
+ // Copy both input stacks to ignore any modifications to them during the upgrading process
+ ItemStack result = ((IWorkbenchItem)item).applyUpgrade(null, original.copy(), upgrade.copy());
+ // It's only a valid 'recipe' if something actually changed
+ if(!ItemStack.areItemStacksEqual(original, result)){
+ recipes.add(new ArcaneWorkbenchRecipe(original, Collections.emptyList(), Collections.emptyList(),
+ Collections.singletonList(upgrade), result));
+ }
+ }
+
+ List applicableSpecialUpgrades = new ArrayList<>();
+
+ for(ItemStack upgrade : specialUpgrades){
+ // Copy both input stacks to ignore any modifications to them during the upgrading process
+ ItemStack result = ((IWorkbenchItem)item).applyUpgrade(null, original.copy(), upgrade.copy());
+ // It's only a valid 'recipe' if something actually changed
+ if(!ItemStack.areItemStacksEqual(original, result)){
+ applicableSpecialUpgrades.add(upgrade);
+ }
+ }
+
+ recipes.add(new ArcaneWorkbenchRecipe(original, Collections.emptyList(), Collections.emptyList(),
+ applicableSpecialUpgrades, original)); // Wands with special upgrades look no different anyway
+
+ }
+ }
+
+ return recipes;
+
+ }
+
+ private static Collection generateChargingRecipes(){
+
+ List recipes = new ArrayList<>();
+
+ List crystals = new ArrayList<>();
+ for(int meta = 0; meta < Element.values().length; meta++) crystals.add(new ItemStack(WizardryItems.magic_crystal, 1, meta));
+ List shard = Collections.singletonList(new ItemStack(WizardryItems.crystal_shard));
+ List grandCrystal = Collections.singletonList(new ItemStack(WizardryItems.grand_crystal));
+
+ for(Item chargeable : WizardryRecipes.getChargeableItems()){
+
+ if(!(chargeable instanceof IManaStoringItem)) throw new IllegalArgumentException("Item to be charged must be an instance of IManaStoringItem");
+
+ ItemStack input = new ItemStack(chargeable);
+ ((IManaStoringItem)chargeable).setMana(input, 0);
+
+ ItemStack result = new ItemStack(chargeable);
+ ((IManaStoringItem)chargeable).setMana(result, Constants.MANA_PER_CRYSTAL);
+ recipes.add(new ArcaneWorkbenchRecipe(input, Collections.emptyList(), crystals, Collections.emptyList(), result));
+
+ result = new ItemStack(chargeable);
+ ((IManaStoringItem)chargeable).setMana(result, Constants.MANA_PER_SHARD);
+ recipes.add(new ArcaneWorkbenchRecipe(input, Collections.emptyList(), shard, Collections.emptyList(), result));
+
+ result = new ItemStack(chargeable);
+ ((IManaStoringItem)chargeable).setMana(result, Constants.GRAND_CRYSTAL_MANA);
+ recipes.add(new ArcaneWorkbenchRecipe(input, Collections.emptyList(), grandCrystal, Collections.emptyList(), result));
+ }
+
+ return recipes;
+
+ }
+
+ private static Collection generateScrollRecipes(){
+
+ List recipes = new ArrayList<>();
+
+ ItemStack blankScroll = new ItemStack(WizardryItems.blank_scroll);
+
+ // We need not make people register these manually since spells already have control over what they can be put on
+ List- spellBooks = Streams.stream(Item.REGISTRY).filter(i -> i instanceof ItemSpellBook).collect(Collectors.toList());
+ List
- scrolls = Streams.stream(Item.REGISTRY).filter(i -> i instanceof ItemScroll).collect(Collectors.toList());
+
+ for(Spell spell : Spell.getAllSpells()){
+ for(Item spellBook : spellBooks){
+ for(Item scroll : scrolls){
+ if(spell.applicableForItem(spellBook) && spell.applicableForItem(scroll)){
+ List books = Collections.singletonList(new ItemStack(WizardryItems.spell_book, 1, spell.metadata()));
+ ItemStack result = new ItemStack(WizardryItems.scroll, 1, spell.metadata());
+ recipes.add(new ArcaneWorkbenchRecipe(blankScroll, books, spell.getCost(), Collections.emptyList(), result));
+ }
+ }
+ }
+ }
+
+ return recipes;
+
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java
new file mode 100644
index 00000000..a911bd1a
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/integration/jei/ArcaneWorkbenchTransferHandler.java
@@ -0,0 +1,52 @@
+package electroblob.wizardry.integration.jei;
+
+import electroblob.wizardry.inventory.ContainerArcaneWorkbench;
+import mezz.jei.api.recipe.transfer.IRecipeTransferInfo;
+import net.minecraft.inventory.Slot;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JEI recipe transfer handler for the arcane workbench. This differs from a standard recipe handler in that it returns
+ * the active bookshelf (virtual) slots from the workbench as part of the inventory slots, and does not require complete
+ * sets of ingredients.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.3
+ */
+public class ArcaneWorkbenchTransferHandler implements IRecipeTransferInfo {
+
+ @Override
+ public Class getContainerClass(){
+ return ContainerArcaneWorkbench.class;
+ }
+
+ @Override
+ public String getRecipeCategoryUid(){
+ return ArcaneWorkbenchRecipeCategory.UID;
+ }
+
+ @Override
+ public boolean canHandle(ContainerArcaneWorkbench container){
+ return true;
+ }
+
+ @Override
+ public boolean requireCompleteSets(){
+ return false; // Arcane workbench maths doesn't work like crafting maths!
+ }
+
+ @Override
+ public List getRecipeSlots(ContainerArcaneWorkbench container){
+ return container.inventorySlots.subList(0, ContainerArcaneWorkbench.UPGRADE_SLOT + 1);
+ }
+
+ @Override
+ public List getInventorySlots(ContainerArcaneWorkbench container){
+ List slots = new ArrayList<>(container.inventorySlots.subList(ContainerArcaneWorkbench.UPGRADE_SLOT + 1, ContainerArcaneWorkbench.UPGRADE_SLOT + 37));
+ slots.addAll(container.getBookshelfSlots());
+ return slots;
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java b/src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java
new file mode 100644
index 00000000..a5b050f2
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/integration/jei/WizardryJEIPlugin.java
@@ -0,0 +1,56 @@
+package electroblob.wizardry.integration.jei;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.item.IConjuredItem;
+import electroblob.wizardry.registry.WizardryBlocks;
+import mezz.jei.api.IModPlugin;
+import mezz.jei.api.IModRegistry;
+import mezz.jei.api.ISubtypeRegistry;
+import mezz.jei.api.JEIPlugin;
+import mezz.jei.api.ingredients.IIngredientBlacklist;
+import mezz.jei.api.recipe.IRecipeCategoryRegistration;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+
+@JEIPlugin
+public class WizardryJEIPlugin implements IModPlugin {
+
+ @Override
+ public void registerCategories(IRecipeCategoryRegistration registry){
+
+ if(!Wizardry.settings.jeiIntegration) return;
+
+ registry.addRecipeCategories(new ArcaneWorkbenchRecipeCategory(registry));
+ }
+
+ @Override
+ public void register(IModRegistry registry){
+
+ if(!Wizardry.settings.jeiIntegration) return;
+
+ // Add arcane workbench as the item required to use arcane workbench recipes
+ registry.addRecipeCatalyst(new ItemStack(WizardryBlocks.arcane_workbench), ArcaneWorkbenchRecipeCategory.UID);
+
+ registry.addRecipes(ArcaneWorkbenchRecipeCategory.generateRecipes(), ArcaneWorkbenchRecipeCategory.UID);
+
+ registry.getRecipeTransferRegistry().addRecipeTransferHandler(new ArcaneWorkbenchTransferHandler());
+
+ // Hide conjured items from JEI
+ IIngredientBlacklist blacklist = registry.getJeiHelpers().getIngredientBlacklist();
+ for(Item item : Item.REGISTRY){
+ if(item instanceof IConjuredItem) blacklist.addIngredientToBlacklist(new ItemStack(item));
+ }
+
+ }
+
+ @Override
+ public void registerItemSubtypes(ISubtypeRegistry subtypeRegistry){
+
+ if(!Wizardry.settings.jeiIntegration) return;
+
+ // TODO: Probably need to distinguish between normal/legendary armour, at the very least
+ // ... that being said, we may well be changing that mechanic anyway
+
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java
index 3ef290d7..771d3146 100644
--- a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java
+++ b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java
@@ -197,12 +197,8 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl
// Show however many spell book slots are necessary
for(int i = 0; i < spellSlots; i++){
-
- float angle = i * (2 * (float)Math.PI)/spellSlots;
- int x = centreX + Math.round(SLOT_RADIUS * MathHelper.sin(angle));
- // -cos because +y is downwards
- int y = centreY + Math.round(SLOT_RADIUS * -MathHelper.cos(angle));
-
+ int x = centreX + getBookSlotXOffset(i, spellSlots);
+ int y = centreY + getBookSlotYOffset(i, spellSlots);
showSlot(i, x, y);
}
@@ -216,6 +212,20 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl
}
}
+ /** Returns the x offset (relative to the central slot) of the ith book slot when the total number of book slots is
+ * equal to {@code bookSlotCount}. */
+ public static int getBookSlotXOffset(int i, int bookSlotCount){
+ float angle = i * (2 * (float)Math.PI) / bookSlotCount;
+ return Math.round(SLOT_RADIUS * MathHelper.sin(angle));
+ }
+
+ /** Returns the y offset (relative to the central slot) of the ith book slot when the total number of book slots is
+ * equal to {@code bookSlotCount}. */
+ public static int getBookSlotYOffset(int i, int bookSlotCount){
+ float angle = i * (2 * (float)Math.PI) / bookSlotCount;
+ return Math.round(SLOT_RADIUS * -MathHelper.cos(angle)); // -cos because +y is downwards
+ }
+
// FIXME: Shift-clicking a stack of special upgrades when in the arcane workbench causes the whole stack to be
// transferred when it should be just one (this is a bug with vanilla as well - try putting a stack of
// bottles into a brewing stand). I have at least made it so only one gets used now, so it has no impact on
diff --git a/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java b/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java
index e85fd314..146246e3 100644
--- a/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java
+++ b/src/main/java/electroblob/wizardry/item/IWorkbenchItem.java
@@ -5,6 +5,8 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
+import javax.annotation.Nullable;
+
/**
* Items that implement this interface may be placed in the central slot of the arcane workbench as long as
* {@link IWorkbenchItem#canPlace(ItemStack)} returns true. The number of spell book slots displayed is also specified
@@ -37,7 +39,7 @@ public interface IWorkbenchItem {
* Can be 0, but must not be negative.
*/
int getSpellSlotCount(ItemStack stack);
-
+
/**
* Called when this item is in the central slot of an arcane workbench and the apply button is pressed. Items must
* implement this method to define what happens when the apply button is pressed. Note that {@link SpellBindEvent}
@@ -60,5 +62,20 @@ public interface IWorkbenchItem {
* @return True if the workbench tooltip should be shown, false if not.
*/
boolean showTooltip(ItemStack stack);
+
+ /**
+ * Applies the given upgrade to this wand. This method is responsible for all checks including tier, progression,
+ * upgrade stack limits, etc. Subclasses are responsible for calling this (usually from
+ * {@link IWorkbenchItem#onApplyButtonPressed(EntityPlayer, Slot, Slot, Slot, Slot[])}), but it has been extracted
+ * as an interface method here for use by JEI 'recipes'.
+ * @param player The player doing the upgrading, or null during JEI recipe lookup (mainly used for advancements)
+ * @param stack The stack being upgraded (it is guaranteed that {@code this == stack.getItem()})
+ * @param upgrade The upgrade item stack being applied. This method is responsible for consuming it!
+ * @return The resulting upgraded wand stack. In many cases, this is simply the input {@code stack}, which has
+ * had its NBT modified. If the given upgrade cannot be applied, simply return the input {@code stack}.
+ */
+ default ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack stack, ItemStack upgrade){
+ return stack;
+ }
}
diff --git a/src/main/java/electroblob/wizardry/item/ItemWand.java b/src/main/java/electroblob/wizardry/item/ItemWand.java
index c37878d4..8272098a 100644
--- a/src/main/java/electroblob/wizardry/item/ItemWand.java
+++ b/src/main/java/electroblob/wizardry/item/ItemWand.java
@@ -45,6 +45,7 @@ import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
+import javax.annotation.Nullable;
import java.util.List;
import java.util.Random;
@@ -625,97 +626,110 @@ public class ItemWand extends Item implements IWorkbenchItem, ISpellCastingItem,
}
@Override
- public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){
-
- boolean changed = false;
-
+ public ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack wand, ItemStack upgrade){
+
// Upgrades wand if necessary. Damage is copied, preserving remaining durability,
// and also the entire NBT tag compound.
- if(upgrade.getStack().getItem() == WizardryItems.arcane_tome){
+ if(upgrade.getItem() == WizardryItems.arcane_tome){
- Tier tier = Tier.values()[upgrade.getStack().getItemDamage()];
+ Tier tier = Tier.values()[upgrade.getItemDamage()];
// Checks the wand upgrade is for the tier above the wand's tier, and that either the wand has enough
// progression or the player is in creative mode.
- // It is guaranteed that: this == centre.getStack().getItem()
- if((player.isCreative() || Wizardry.settings.legacyWandLevelling
- || WandHelper.getProgression(centre.getStack()) >= tier.progression)
+ if((player == null || player.isCreative() || Wizardry.settings.legacyWandLevelling
+ || WandHelper.getProgression(wand) >= tier.progression)
&& tier.ordinal() - 1 == this.tier.ordinal()){
// We're not carrying over excess progression for now, but if we do want to, this is how
// if(!Wizardry.settings.legacyWandLevelling){
// // Easy way to carry excess progression over to the new stack
-// WandHelper.setProgression(centre.getStack(), WandHelper.getProgression(centre.getStack()) - tier.progression);
+// WandHelper.setProgression(wand, WandHelper.getProgression(wand) - tier.progression);
// }
- WandHelper.setProgression(centre.getStack(), 0);
+ WandHelper.setProgression(wand, 0);
ItemStack newWand = new ItemStack(WizardryItems.getWand(tier, this.element));
- newWand.setTagCompound(centre.getStack().getTagCompound());
+ newWand.setTagCompound(wand.getTagCompound());
// This needs to be done after copying the tag compound so the mana capacity for the new wand
// takes storage upgrades into account
// Note the usage of the new wand item and not 'this' to ensure the correct capacity is used
- ((IManaStoringItem)newWand.getItem()).setMana(newWand, this.getMana(centre.getStack()));
+ ((IManaStoringItem)newWand.getItem()).setMana(newWand, this.getMana(wand));
- centre.putStack(newWand);
- upgrade.decrStackSize(1);
-
- changed = true;
+ upgrade.shrink(1);
+
+ return newWand;
}
- }else if(WandHelper.isWandUpgrade(upgrade.getStack().getItem())){
+ }else if(WandHelper.isWandUpgrade(upgrade.getItem())){
// Special upgrades
- Item specialUpgrade = upgrade.getStack().getItem();
+ Item specialUpgrade = upgrade.getItem();
- if(WandHelper.getTotalUpgrades(centre.getStack()) < this.tier.upgradeLimit
- && WandHelper.getUpgradeLevel(centre.getStack(), specialUpgrade) < Constants.UPGRADE_STACK_LIMIT){
+ if(WandHelper.getTotalUpgrades(wand) < this.tier.upgradeLimit
+ && WandHelper.getUpgradeLevel(wand, specialUpgrade) < Constants.UPGRADE_STACK_LIMIT){
// Used to preserve existing mana when upgrading storage rather than creating free mana.
- int prevMana = this.getMana(centre.getStack());
+ int prevMana = this.getMana(wand);
- WandHelper.applyUpgrade(centre.getStack(), specialUpgrade);
+ WandHelper.applyUpgrade(wand, specialUpgrade);
// Special behaviours for specific upgrades
if(specialUpgrade == WizardryItems.storage_upgrade){
- this.setMana(centre.getStack(), prevMana);
-
+ this.setMana(wand, prevMana);
+
}else if(specialUpgrade == WizardryItems.attunement_upgrade){
- int newSlotCount = BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(centre.getStack(),
+ int newSlotCount = BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(wand,
WizardryItems.attunement_upgrade);
-
- Spell[] spells = WandHelper.getSpells(centre.getStack());
+
+ Spell[] spells = WandHelper.getSpells(wand);
Spell[] newSpells = new Spell[newSlotCount];
for(int i = 0; i < newSpells.length; i++){
newSpells[i] = i < spells.length && spells[i] != null ? spells[i] : Spells.none;
}
- WandHelper.setSpells(centre.getStack(), newSpells);
+ WandHelper.setSpells(wand, newSpells);
- int[] cooldowns = WandHelper.getCooldowns(centre.getStack());
+ int[] cooldowns = WandHelper.getCooldowns(wand);
int[] newCooldowns = new int[newSlotCount];
if(cooldowns.length > 0){
System.arraycopy(cooldowns, 0, newCooldowns, 0, cooldowns.length);
}
- WandHelper.setCooldowns(centre.getStack(), newCooldowns);
+ WandHelper.setCooldowns(wand, newCooldowns);
}
- upgrade.decrStackSize(1);
- WizardryAdvancementTriggers.special_upgrade.triggerFor(player);
+ upgrade.shrink(1);
- if(WandHelper.getTotalUpgrades(centre.getStack()) == Tier.MASTER.upgradeLimit){
- WizardryAdvancementTriggers.max_out_wand.triggerFor(player);
+ if(player != null){
+
+ WizardryAdvancementTriggers.special_upgrade.triggerFor(player);
+
+ if(WandHelper.getTotalUpgrades(wand) == Tier.MASTER.upgradeLimit){
+ WizardryAdvancementTriggers.max_out_wand.triggerFor(player);
+ }
}
-
- changed = true;
+
}
}
+ return wand;
+ }
+
+ @Override
+ public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){
+
+ boolean changed = false; // Used for advancements
+
+ if(upgrade.getHasStack()){
+ ItemStack original = centre.getStack().copy();
+ centre.putStack(this.applyUpgrade(player, centre.getStack(), upgrade.getStack()));
+ changed = ItemStack.areItemStacksEqual(centre.getStack(), original);
+ }
+
// Reads NBT spell metadata array to variable, edits this, then writes it back to NBT.
// Original spells are preserved; if a slot is left empty the existing spell binding will remain.
// Accounts for spells which cannot be applied because they are above the wand's tier; these spells
diff --git a/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java b/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java
index e70301c1..9b5ff051 100644
--- a/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java
+++ b/src/main/java/electroblob/wizardry/item/ItemWizardArmour.java
@@ -38,6 +38,7 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
+import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@@ -247,25 +248,35 @@ public class ItemWizardArmour extends ItemArmor implements IWorkbenchItem, IMana
return 0; // Doesn't have any spell slots!
}
+ @Override
+ public ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack stack, ItemStack upgrade){
+
+ // Applies legendary upgrade
+ if(upgrade.getItem() == WizardryItems.armour_upgrade){
+
+ if(!stack.hasTagCompound()){
+ stack.setTagCompound(new NBTTagCompound());
+ }
+
+ if(!stack.getTagCompound().hasKey("legendary")){
+ stack.getTagCompound().setBoolean("legendary", true);
+ upgrade.shrink(1);
+ if(player != null) WizardryAdvancementTriggers.legendary.triggerFor(player);
+ }
+ }
+
+ return stack;
+ }
+
@Override
public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){
boolean changed = false;
-
- // Applies legendary upgrade
- if(upgrade.getStack().getItem() == WizardryItems.armour_upgrade){
-
- if(!centre.getStack().hasTagCompound()){
- centre.getStack().setTagCompound(new NBTTagCompound());
- }
-
- if(!centre.getStack().getTagCompound().hasKey("legendary")){
-
- centre.getStack().getTagCompound().setBoolean("legendary", true);
- upgrade.decrStackSize(1);
- WizardryAdvancementTriggers.legendary.triggerFor(player);
- changed = true;
- }
+
+ if(upgrade.getHasStack()){
+ ItemStack original = centre.getStack().copy();
+ centre.putStack(this.applyUpgrade(player, centre.getStack(), upgrade.getStack()));
+ changed = ItemStack.areItemStacksEqual(centre.getStack(), original);
}
// Charges armour by appropriate amount
diff --git a/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java b/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java
index 869088ff..5d2b5e7b 100644
--- a/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java
+++ b/src/main/java/electroblob/wizardry/registry/WizardryRecipes.java
@@ -14,8 +14,9 @@ import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.registries.IForgeRegistry;
-import java.util.LinkedList;
-import java.util.Queue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
/**
* Class responsible for defining and registering wizardry's non-JSON recipes (i.e. smelting recipes and dynamic
@@ -29,14 +30,27 @@ public final class WizardryRecipes {
private WizardryRecipes(){} // No instances!
- private static final Queue
- chargingRecipeQueue = new LinkedList<>();
+ private static final List
- chargeableItems = new ArrayList<>();
+
+ private static boolean registered;
/** Adds the given item to the list of items that can be charged using mana flasks. Dynamic charging recipes
* will be added for these items during {@code RegistryEvent.Register}. The item must implement
* {@link IManaStoringItem} for the recipes to work correctly. This method should be called from the item's
* constructor. */
public static void addToManaFlaskCharging(Item item){
- chargingRecipeQueue.offer(item);
+
+ if(registered){
+ Wizardry.logger.warn("Tried to add an item to mana flask charging after it was registered, this will do nothing!");
+ return;
+ }
+
+ chargeableItems.add(item);
+ }
+
+ /** Returns an unmodifiable view of all registered items that can be charged with mana flasks. */
+ public static List
- getChargeableItems(){
+ return Collections.unmodifiableList(chargeableItems);
}
/** Now only deals with the dynamic crafting recipes and the smelting recipes. */
@@ -49,11 +63,7 @@ public final class WizardryRecipes {
// Mana flask recipes
- Item chargeable;
-
- while(!chargingRecipeQueue.isEmpty()){
- // Use remove() and not poll() because the queue shouldn't be empty in here
- chargeable = chargingRecipeQueue.remove();
+ for(Item chargeable : chargeableItems){
registry.register(new RecipeRechargeWithFlask(chargeable, (ItemManaFlask)WizardryItems.small_mana_flask)
.setRegistryName(new ResourceLocation(Wizardry.MODID, "recipes/small_flask_" + chargeable.getRegistryName().getPath())));
@@ -65,6 +75,9 @@ public final class WizardryRecipes {
.setRegistryName(new ResourceLocation(Wizardry.MODID, "recipes/large_flask_" + chargeable.getRegistryName().getPath())));
}
+
+ registered = true;
+
}
}
diff --git a/src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png b/src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png
new file mode 100644
index 00000000..9bd25c58
Binary files /dev/null and b/src/main/resources/assets/ebwizardry/textures/gui/container/arcane_workbench_jei_background.png differ