From ff1913b15f2abb76340dd52b39846ed1d35df466 Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:35:52 -0400 Subject: [PATCH 1/9] Implement spellbook copying recipe --- .../recipe/dynamiccopy_spellbook.json | 4 + .../common/recipe/CopySpellbookRecipe.java | 92 +++++++++++++++++++ .../common/recipe/HexRecipeStuffRegistry.java | 2 + .../datagen/recipe/HexplatRecipes.java | 2 + .../recipe/dynamiccopy_spellbook.json | 4 + .../recipe/dynamiccopy_spellbook.json | 4 + 6 files changed, 108 insertions(+) create mode 100644 Common/src/generated/resources/data/hexcasting/recipe/dynamiccopy_spellbook.json create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java create mode 100644 Fabric/src/generated/resources/data/hexcasting/recipe/dynamiccopy_spellbook.json create mode 100644 Neoforge/src/generated/resources/data/hexcasting/recipe/dynamiccopy_spellbook.json diff --git a/Common/src/generated/resources/data/hexcasting/recipe/dynamiccopy_spellbook.json b/Common/src/generated/resources/data/hexcasting/recipe/dynamiccopy_spellbook.json new file mode 100644 index 000000000..9b052f350 --- /dev/null +++ b/Common/src/generated/resources/data/hexcasting/recipe/dynamiccopy_spellbook.json @@ -0,0 +1,4 @@ +{ + "type": "hexcasting:copy_spellbook", + "category": "misc" +} \ No newline at end of file diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java new file mode 100644 index 000000000..cd786c37d --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java @@ -0,0 +1,92 @@ +package at.petrak.hexcasting.common.recipe; + +import at.petrak.hexcasting.common.lib.HexDataComponents; +import at.petrak.hexcasting.common.lib.HexItems; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +public class CopySpellbookRecipe extends CustomRecipe { + public static final SimpleCraftingRecipeSerializer SERIALIZER = + new SimpleCraftingRecipeSerializer<>(CopySpellbookRecipe::new); + + public CopySpellbookRecipe(CraftingBookCategory category) { + super(category); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return width * height >= 2; + } + + @Override + public boolean matches(CraftingInput container, Level level) { + boolean foundBreath = false; + boolean foundOriginal = false; + boolean foundBlank = false; + + for (int i = 0; i < container.size(); i++) { + var stack = container.getItem(i); + if (stack.is(Items.DRAGON_BREATH)) { + if (foundBreath) return false; + foundBreath = true; + } else if (stack.is(HexItems.SPELLBOOK.get())) { + if (stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { + if (foundOriginal) return false; + foundOriginal = true; + } else { + if (foundBlank) return false; + foundBlank = true; + } + } else { + return false; + } + } + + return foundBreath && foundOriginal && foundBlank; + } + + @Override + public @NotNull ItemStack assemble(CraftingInput inv, HolderLookup.RegistryLookup.@NotNull Provider registryProvider) { + ItemStack output = ItemStack.EMPTY; + + for (int i = 0; i < inv.size(); i++) { + var stack = inv.getItem(i); + if (stack.is(HexItems.SPELLBOOK.get()) && stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { + output = stack.copy(); + break; + } + } + + if (!output.isEmpty()) { + output.setCount(1); + } + + return output; + } + + @Override + public @NotNull NonNullList getRemainingItems(@NotNull CraftingInput craftingInput) { + NonNullList remainingItems = NonNullList.withSize(craftingInput.size(), ItemStack.EMPTY); + + for(int i = 0; i < remainingItems.size(); ++i) { + ItemStack stack = craftingInput.getItem(i); + if (stack.getItem().hasCraftingRemainingItem()) { + remainingItems.set(i, new ItemStack(stack.getItem().getCraftingRemainingItem())); + } else if (stack.is(HexItems.SPELLBOOK.get()) && stack.get(HexDataComponents.SPELLBOOK_PAGES.get()) != null) { + remainingItems.set(i, stack.copyWithCount(1)); + } + } + + return remainingItems; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return SERIALIZER; + } +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java index 1e1c26aa8..74fd62215 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java @@ -30,6 +30,8 @@ public static void register() { "seal_focus", () -> SealThingsRecipe.FOCUS_SERIALIZER); public static final Supplier> SEAL_SPELLBOOK = REGISTER_SERIALIZER.register( "seal_spellbook", () -> SealThingsRecipe.SPELLBOOK_SERIALIZER); + public static final Supplier> COPY_SPELLBOOK = REGISTER_SERIALIZER.register( + "copy_spellbook", () -> CopySpellbookRecipe.SERIALIZER); public static Supplier> BRAINSWEEP_TYPE = REGISTER_RECIPE_TYPE.register("brainsweep", () -> new RecipeType() { diff --git a/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java b/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java index 74471a67e..f77b5ffe9 100644 --- a/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java +++ b/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java @@ -9,6 +9,7 @@ import at.petrak.hexcasting.common.items.pigment.ItemPridePigment; import at.petrak.hexcasting.common.lib.HexBlocks; import at.petrak.hexcasting.common.lib.HexItems; +import at.petrak.hexcasting.common.recipe.CopySpellbookRecipe; import at.petrak.hexcasting.common.recipe.SealThingsRecipe; import at.petrak.hexcasting.common.recipe.ingredient.brainsweep.EntityTypeIngredient; import at.petrak.hexcasting.common.recipe.ingredient.brainsweep.VillagerIngredient; @@ -87,6 +88,7 @@ public HexplatRecipes(PackOutput output, CompletableFuture Date: Sat, 1 Aug 2026 21:50:57 -0400 Subject: [PATCH 2/9] Fix sealing recipe voiding anything else added to the crafting grid --- .../hexcasting/common/recipe/SealThingsRecipe.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/SealThingsRecipe.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/SealThingsRecipe.java index 315fadb54..911f92c08 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/recipe/SealThingsRecipe.java +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/SealThingsRecipe.java @@ -35,7 +35,7 @@ public boolean canCraftInDimensions(int width, int height) { @Override public boolean matches(CraftingInput container, Level level) { - boolean foundComb = false; + boolean foundSealant = false; boolean foundSealee = false; for (int i = 0; i < container.size(); i++) { @@ -44,12 +44,14 @@ public boolean matches(CraftingInput container, Level level) { if (foundSealee) return false; foundSealee = true; } else if (stack.is(HexTags.Items.SEAL_MATERIALS)) { - if (foundComb) return false; - foundComb = true; + if (foundSealant) return false; + foundSealant = true; + } else { + return false; } } - return foundComb && foundSealee; + return foundSealant && foundSealee; } @Override From c51d8f10257b0aabe576b764d66ff669d0306fb8 Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:51:20 -0400 Subject: [PATCH 3/9] Use tag for spellbook-copying material --- .../tags/item/spellbook_copy_materials.json | 5 +++++ .../java/at/petrak/hexcasting/api/mod/HexTags.java | 1 + .../hexcasting/common/recipe/CopySpellbookRecipe.java | 11 ++++++----- .../hexcasting/datagen/tag/HexItemTagProvider.java | 2 ++ .../tags/item/spellbook_copy_materials.json | 5 +++++ .../tags/item/spellbook_copy_materials.json | 5 +++++ 6 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 Common/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json create mode 100644 Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json create mode 100644 Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json diff --git a/Common/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json b/Common/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json new file mode 100644 index 000000000..1c508bfd3 --- /dev/null +++ b/Common/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:dragon_breath" + ] +} \ No newline at end of file diff --git a/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java b/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java index 8230034ac..4bbd0855d 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java +++ b/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java @@ -19,6 +19,7 @@ public static final class Items { public static final TagKey PHIAL_BASE = create("phial_base"); public static final TagKey GRANTS_ROOT_ADVANCEMENT = create("grants_root_advancement"); public static final TagKey SEAL_MATERIALS = create("seal_materials"); + public static final TagKey SPELLBOOK_COPY_MATERIALS = create("spellbook_copy_materials"); public static final TagKey IMPETI = create("impeti"); public static final TagKey DIRECTRICES = create("directrices"); diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java index cd786c37d..1c0095989 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java @@ -1,5 +1,6 @@ package at.petrak.hexcasting.common.recipe; +import at.petrak.hexcasting.api.mod.HexTags; import at.petrak.hexcasting.common.lib.HexDataComponents; import at.petrak.hexcasting.common.lib.HexItems; import net.minecraft.core.HolderLookup; @@ -25,15 +26,15 @@ public boolean canCraftInDimensions(int width, int height) { @Override public boolean matches(CraftingInput container, Level level) { - boolean foundBreath = false; + boolean foundCopyMaterial = false; boolean foundOriginal = false; boolean foundBlank = false; for (int i = 0; i < container.size(); i++) { var stack = container.getItem(i); - if (stack.is(Items.DRAGON_BREATH)) { - if (foundBreath) return false; - foundBreath = true; + if (stack.is(HexTags.Items.SPELLBOOK_COPY_MATERIALS)) { + if (foundCopyMaterial) return false; + foundCopyMaterial = true; } else if (stack.is(HexItems.SPELLBOOK.get())) { if (stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { if (foundOriginal) return false; @@ -47,7 +48,7 @@ public boolean matches(CraftingInput container, Level level) { } } - return foundBreath && foundOriginal && foundBlank; + return foundCopyMaterial && foundOriginal && foundBlank; } @Override diff --git a/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java b/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java index 710c88f32..b4003bbfd 100644 --- a/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java +++ b/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java @@ -45,6 +45,8 @@ protected void addTags(HolderLookup.Provider provider) { HexItems.CHARGED_AMETHYST.get(), HexItems.CREATIVE_UNLOCKER.get()); add(tag(HexTags.Items.SEAL_MATERIALS), Items.HONEYCOMB); + add(tag(HexTags.Items.SPELLBOOK_COPY_MATERIALS), + Items.DRAGON_BREATH); this.copy(HexTags.Blocks.EDIFIED_LOGS, HexTags.Items.EDIFIED_LOGS); this.copy(HexTags.Blocks.EDIFIED_PLANKS, HexTags.Items.EDIFIED_PLANKS); diff --git a/Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json b/Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json new file mode 100644 index 000000000..1c508bfd3 --- /dev/null +++ b/Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:dragon_breath" + ] +} \ No newline at end of file diff --git a/Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json b/Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json new file mode 100644 index 000000000..1c508bfd3 --- /dev/null +++ b/Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_copy_materials.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:dragon_breath" + ] +} \ No newline at end of file From 9bea5d874d71d9e324c44a83edcfc3600281a2d1 Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sat, 1 Aug 2026 22:11:33 -0400 Subject: [PATCH 4/9] Mention spellbook copying in guidebook --- .../resources/assets/hexcasting/lang/en_us.flatten.json5 | 2 ++ .../thehexbook/en_us/entries/items/spellbook.json | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 index 4ebfa8281..d25ec65f5 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -1609,6 +1609,8 @@ "1": "A $(l:items/spellbook)$(item)Spellbook/$ is the culmination of my art-- it acts like an entire library of $(l:items/focus)$(item)Foci/$. Up to $(thing)sixty-four/$ of them, to be exact.$(br2)Each page can hold a single iota, and I can select the active page (the page that iotas are saved to and copied from) by sneak-scrolling while holding it, or simply holding it in my off-hand and scrolling while casting a _Hex.", "2": "Like a $(l:items/focus)$(item)Focus/$, there exists a simple method to prevent accidental overwriting. Crafting it with a $(item)Honeycomb/$ will lacquer the current page, preventing $(l:patterns/readwrite#hexcasting:write)$(action)Scribe's Gambit/$ from modifying its contents. Also like a $(l:items/focus)$(item)Focus/$, using $(l:patterns/spells/hexcasting#hexcasting:erase)$(action)Erase Item/$ will remove the lacquer along with the page's contents.$(br2)I can also name each page individually in an anvil. Naming it will change only the name of the currently selected page, for easy browsing.", "crafting.desc": "$(italic)Wizards love words. Most of them read a great deal, and indeed one strong sign of a potential wizard is the inability to get to sleep without reading something first.", + "3.title": "Making Copies", + "3": "Exposing a spellbook to $(item)Dragon’s Breath/$ has an unusual effect: for a time, the iotas stored in the pages become less strongly bound to them, drifting around and potentially forming echoes of themselves in receptive objects nearby.$(br2)I can exploit this behavior to easily copy the contents of one spellbook into another empty one, using nothing but my crafting table.", }, scroll: { diff --git a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/items/spellbook.json b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/items/spellbook.json index 1713ad1db..5cd5e5fb9 100644 --- a/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/items/spellbook.json +++ b/Common/src/main/resources/assets/hexcasting/patchouli_books/thehexbook/en_us/entries/items/spellbook.json @@ -17,6 +17,11 @@ "type": "patchouli:crafting", "recipe": "hexcasting:spellbook", "text": "hexcasting.page.spellbook.crafting.desc" + }, + { + "type": "patchouli:text", + "title": "hexcasting.page.spellbook.3.title", + "text": "hexcasting.page.spellbook.3" } ] } From 2b3c723553983814823a8d6059b76b49ed464898 Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sat, 1 Aug 2026 22:11:41 -0400 Subject: [PATCH 5/9] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be5e139f..f76dadf05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Added Simulate, which causes the next pattern drawn to be simulated (to check for mishaps) rather than executed ([#1194](https://github.com/FallingColors/HexMod/pull/1194)) @Robotgiggle - Added the `hex_unbreakable` tag for blocks that should be immune to Break Block regardless of the configured mining tier ([#1186](https://github.com/FallingColors/HexMod/pull/1186)) @Robotgiggle @slava110 - Added a new Ancient Cypher hex that impulses nearby items towards the caster ([#1106](https://github.com/FallingColors/HexMod/pull/1106)) @IridescentVoid +- Added a recipe to copy a spellbook's contents into another blank spellbook using dragon's breath ([#1231](https://github.com/FallingColors/HexMod/pull/1231)) @Robotgiggle ### Changed @@ -26,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Fixed Entity Iota comparison to use `equals` on entity IDs instead of reference equality ([#1101](https://github.com/FallingColors/HexMod/pull/1101)) @IridescentVoid - Corrected field names of the codec for Pattern Iotas and add a graceful fallback for upgrading ([#1120](https://github.com/FallingColors/HexMod/pull/1120), [#1131](https://github.com/FallingColors/HexMod/pull/1131), [#1140](https://github.com/FallingColors/HexMod/pull/1140)) @Master-Bw3 @IridescentVoid +- Fixed the iota-holder sealing recipe also consuming anything else placed into the crafting grid ([#1231](https://github.com/FallingColors/HexMod/pull/1231)) @Robotgiggle ### Internal From b0128eafa9e745b95a3fe6e6c29190ce240642a3 Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:27:29 -0400 Subject: [PATCH 6/9] Clarify crafting recipe --- .../main/resources/assets/hexcasting/lang/en_us.flatten.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 index d25ec65f5..8293008ef 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -1610,7 +1610,7 @@ "2": "Like a $(l:items/focus)$(item)Focus/$, there exists a simple method to prevent accidental overwriting. Crafting it with a $(item)Honeycomb/$ will lacquer the current page, preventing $(l:patterns/readwrite#hexcasting:write)$(action)Scribe's Gambit/$ from modifying its contents. Also like a $(l:items/focus)$(item)Focus/$, using $(l:patterns/spells/hexcasting#hexcasting:erase)$(action)Erase Item/$ will remove the lacquer along with the page's contents.$(br2)I can also name each page individually in an anvil. Naming it will change only the name of the currently selected page, for easy browsing.", "crafting.desc": "$(italic)Wizards love words. Most of them read a great deal, and indeed one strong sign of a potential wizard is the inability to get to sleep without reading something first.", "3.title": "Making Copies", - "3": "Exposing a spellbook to $(item)Dragon’s Breath/$ has an unusual effect: for a time, the iotas stored in the pages become less strongly bound to them, drifting around and potentially forming echoes of themselves in receptive objects nearby.$(br2)I can exploit this behavior to easily copy the contents of one spellbook into another empty one, using nothing but my crafting table.", + "3": "Exposing a $(item)Spellbook/$ to $(item)Dragon’s Breath/$ has an unusual effect: for a time, the iotas stored in the pages become less strongly bound to them, potentially forming echoes of themselves in nearby receptive objects.$(br2)I can exploit this behavior to easily copy the contents of one $(item)Spellbook/$ into another empty one, by simply crafting them together along with some $(item)Dragon's Breath/$.", }, scroll: { From 3258e7ed0ce8a0d801dcd2dca49174fefeac1e7a Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:56:44 -0400 Subject: [PATCH 7/9] Preserve texture variant when copying --- .../common/recipe/CopySpellbookRecipe.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java index 1c0095989..b3627c739 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/CopySpellbookRecipe.java @@ -5,6 +5,7 @@ import at.petrak.hexcasting.common.lib.HexItems; import net.minecraft.core.HolderLookup; import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponentType; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.*; @@ -54,19 +55,20 @@ public boolean matches(CraftingInput container, Level level) { @Override public @NotNull ItemStack assemble(CraftingInput inv, HolderLookup.RegistryLookup.@NotNull Provider registryProvider) { ItemStack output = ItemStack.EMPTY; - + Integer variant = null; for (int i = 0; i < inv.size(); i++) { var stack = inv.getItem(i); - if (stack.is(HexItems.SPELLBOOK.get()) && stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { - output = stack.copy(); - break; + if (stack.is(HexItems.SPELLBOOK.get())) { + if (stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { + output = stack.copyWithCount(1); + } else { + variant = stack.get(HexDataComponents.ITEM_VARIANT.get()); + } } } - - if (!output.isEmpty()) { - output.setCount(1); + if (variant != null && output != ItemStack.EMPTY) { + output.set(HexDataComponents.ITEM_VARIANT.get(), variant); } - return output; } From 891d20c5725635553fc96b51b121ed3717a59815 Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:56:14 -0400 Subject: [PATCH 8/9] Implement spellbook erasing recipe --- .../recipe/dynamicerase_spellbook.json | 4 ++ .../tags/item/spellbook_erase_materials.json | 5 ++ .../at/petrak/hexcasting/api/mod/HexTags.java | 1 + .../common/recipe/EraseSpellbookRecipe.java | 68 +++++++++++++++++++ .../common/recipe/HexRecipeStuffRegistry.java | 2 + .../datagen/recipe/HexplatRecipes.java | 2 + .../datagen/tag/HexItemTagProvider.java | 2 + .../hexcasting/lang/en_us.flatten.json5 | 4 +- .../recipe/dynamicerase_spellbook.json | 4 ++ .../tags/item/spellbook_erase_materials.json | 5 ++ .../recipe/dynamicerase_spellbook.json | 4 ++ .../tags/item/spellbook_erase_materials.json | 5 ++ 12 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 Common/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json create mode 100644 Common/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json create mode 100644 Common/src/main/java/at/petrak/hexcasting/common/recipe/EraseSpellbookRecipe.java create mode 100644 Fabric/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json create mode 100644 Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json create mode 100644 Neoforge/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json create mode 100644 Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json diff --git a/Common/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json b/Common/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json new file mode 100644 index 000000000..3612273f7 --- /dev/null +++ b/Common/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json @@ -0,0 +1,4 @@ +{ + "type": "hexcasting:erase_spellbook", + "category": "misc" +} \ No newline at end of file diff --git a/Common/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json b/Common/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json new file mode 100644 index 000000000..355a91fd0 --- /dev/null +++ b/Common/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:soul_sand" + ] +} \ No newline at end of file diff --git a/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java b/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java index 4bbd0855d..073f9f4e2 100644 --- a/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java +++ b/Common/src/main/java/at/petrak/hexcasting/api/mod/HexTags.java @@ -20,6 +20,7 @@ public static final class Items { public static final TagKey GRANTS_ROOT_ADVANCEMENT = create("grants_root_advancement"); public static final TagKey SEAL_MATERIALS = create("seal_materials"); public static final TagKey SPELLBOOK_COPY_MATERIALS = create("spellbook_copy_materials"); + public static final TagKey SPELLBOOK_ERASE_MATERIALS = create("spellbook_erase_materials"); public static final TagKey IMPETI = create("impeti"); public static final TagKey DIRECTRICES = create("directrices"); diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/EraseSpellbookRecipe.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/EraseSpellbookRecipe.java new file mode 100644 index 000000000..6b3ff7078 --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/EraseSpellbookRecipe.java @@ -0,0 +1,68 @@ +package at.petrak.hexcasting.common.recipe; + +import at.petrak.hexcasting.api.mod.HexTags; +import at.petrak.hexcasting.common.lib.HexDataComponents; +import at.petrak.hexcasting.common.lib.HexItems; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +public class EraseSpellbookRecipe extends CustomRecipe { + public static final SimpleCraftingRecipeSerializer SERIALIZER = + new SimpleCraftingRecipeSerializer<>(EraseSpellbookRecipe::new); + + public EraseSpellbookRecipe(CraftingBookCategory category) { + super(category); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return width * height >= 2; + } + + @Override + public boolean matches(CraftingInput container, Level level) { + boolean foundEraseMaterial = false; + boolean foundSpellbook = false; + + for (int i = 0; i < container.size(); i++) { + var stack = container.getItem(i); + if (stack.is(HexTags.Items.SPELLBOOK_ERASE_MATERIALS)) { + if (foundEraseMaterial) return false; + foundEraseMaterial = true; + } else if (stack.is(HexItems.SPELLBOOK.get()) && stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { + if (foundSpellbook) return false; + foundSpellbook = true; + } else { + return false; + } + } + + return foundEraseMaterial && foundSpellbook; + } + + @Override + public @NotNull ItemStack assemble(CraftingInput inv, HolderLookup.RegistryLookup.@NotNull Provider registryProvider) { + for (int i = 0; i < inv.size(); i++) { + var stack = inv.getItem(i); + if (stack.is(HexItems.SPELLBOOK.get()) && stack.has(HexDataComponents.SPELLBOOK_PAGES.get())) { + var output = new ItemStack(HexItems.SPELLBOOK.get(), 1); + var variant = stack.get(HexDataComponents.ITEM_VARIANT.get()); + if (variant != null) { + output.set(HexDataComponents.ITEM_VARIANT.get(), variant); + } + return output; + } + } + return ItemStack.EMPTY; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return SERIALIZER; + } +} diff --git a/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java b/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java index 74fd62215..dddd680fc 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java +++ b/Common/src/main/java/at/petrak/hexcasting/common/recipe/HexRecipeStuffRegistry.java @@ -32,6 +32,8 @@ public static void register() { "seal_spellbook", () -> SealThingsRecipe.SPELLBOOK_SERIALIZER); public static final Supplier> COPY_SPELLBOOK = REGISTER_SERIALIZER.register( "copy_spellbook", () -> CopySpellbookRecipe.SERIALIZER); + public static final Supplier> ERASE_SPELLBOOK = REGISTER_SERIALIZER.register( + "erase_spellbook", () -> EraseSpellbookRecipe.SERIALIZER); public static Supplier> BRAINSWEEP_TYPE = REGISTER_RECIPE_TYPE.register("brainsweep", () -> new RecipeType() { diff --git a/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java b/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java index f77b5ffe9..e0ae5be92 100644 --- a/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java +++ b/Common/src/main/java/at/petrak/hexcasting/datagen/recipe/HexplatRecipes.java @@ -10,6 +10,7 @@ import at.petrak.hexcasting.common.lib.HexBlocks; import at.petrak.hexcasting.common.lib.HexItems; import at.petrak.hexcasting.common.recipe.CopySpellbookRecipe; +import at.petrak.hexcasting.common.recipe.EraseSpellbookRecipe; import at.petrak.hexcasting.common.recipe.SealThingsRecipe; import at.petrak.hexcasting.common.recipe.ingredient.brainsweep.EntityTypeIngredient; import at.petrak.hexcasting.common.recipe.ingredient.brainsweep.VillagerIngredient; @@ -89,6 +90,7 @@ public void buildRecipes(RecipeOutput recipes) { specialRecipe(recipes, SealThingsRecipe.FOCUS_SERIALIZER, SealThingsRecipe::focus); specialRecipe(recipes, SealThingsRecipe.SPELLBOOK_SERIALIZER, SealThingsRecipe::spellbook); specialRecipe(recipes, CopySpellbookRecipe.SERIALIZER, CopySpellbookRecipe::new); + specialRecipe(recipes, EraseSpellbookRecipe.SERIALIZER, EraseSpellbookRecipe::new); staffRecipe(recipes, HexItems.STAFF_OAK.get(), Items.OAK_PLANKS); staffRecipe(recipes, HexItems.STAFF_BIRCH.get(), Items.BIRCH_PLANKS); diff --git a/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java b/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java index b4003bbfd..0fea1b906 100644 --- a/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java +++ b/Common/src/main/java/at/petrak/hexcasting/datagen/tag/HexItemTagProvider.java @@ -47,6 +47,8 @@ protected void addTags(HolderLookup.Provider provider) { Items.HONEYCOMB); add(tag(HexTags.Items.SPELLBOOK_COPY_MATERIALS), Items.DRAGON_BREATH); + add(tag(HexTags.Items.SPELLBOOK_ERASE_MATERIALS), + Items.SOUL_SAND); this.copy(HexTags.Blocks.EDIFIED_LOGS, HexTags.Items.EDIFIED_LOGS); this.copy(HexTags.Blocks.EDIFIED_PLANKS, HexTags.Items.EDIFIED_PLANKS); diff --git a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 index 8293008ef..bea9c0b5b 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -1609,8 +1609,8 @@ "1": "A $(l:items/spellbook)$(item)Spellbook/$ is the culmination of my art-- it acts like an entire library of $(l:items/focus)$(item)Foci/$. Up to $(thing)sixty-four/$ of them, to be exact.$(br2)Each page can hold a single iota, and I can select the active page (the page that iotas are saved to and copied from) by sneak-scrolling while holding it, or simply holding it in my off-hand and scrolling while casting a _Hex.", "2": "Like a $(l:items/focus)$(item)Focus/$, there exists a simple method to prevent accidental overwriting. Crafting it with a $(item)Honeycomb/$ will lacquer the current page, preventing $(l:patterns/readwrite#hexcasting:write)$(action)Scribe's Gambit/$ from modifying its contents. Also like a $(l:items/focus)$(item)Focus/$, using $(l:patterns/spells/hexcasting#hexcasting:erase)$(action)Erase Item/$ will remove the lacquer along with the page's contents.$(br2)I can also name each page individually in an anvil. Naming it will change only the name of the currently selected page, for easy browsing.", "crafting.desc": "$(italic)Wizards love words. Most of them read a great deal, and indeed one strong sign of a potential wizard is the inability to get to sleep without reading something first.", - "3.title": "Making Copies", - "3": "Exposing a $(item)Spellbook/$ to $(item)Dragon’s Breath/$ has an unusual effect: for a time, the iotas stored in the pages become less strongly bound to them, potentially forming echoes of themselves in nearby receptive objects.$(br2)I can exploit this behavior to easily copy the contents of one $(item)Spellbook/$ into another empty one, by simply crafting them together along with some $(item)Dragon's Breath/$.", + "3.title": "Copying and Erasing", + "3": "Exposing a $(item)Spellbook/$ to $(item)Dragon’s Breath/$ renders the stored iotas volatile, allowing me to capture echoes of them in other receptive objects. I can use this to copy one $(item)Spellbook/$ into another empty one, simply by crafting them together with $(item)Dragon's Breath/$.$(br2)I can also craft a filled $(item)Spellbook/$ together with $(item)Soul Sand/$ to scrub the pages clean, leaving it completely empty.", }, scroll: { diff --git a/Fabric/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json b/Fabric/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json new file mode 100644 index 000000000..3612273f7 --- /dev/null +++ b/Fabric/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json @@ -0,0 +1,4 @@ +{ + "type": "hexcasting:erase_spellbook", + "category": "misc" +} \ No newline at end of file diff --git a/Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json b/Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json new file mode 100644 index 000000000..355a91fd0 --- /dev/null +++ b/Fabric/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:soul_sand" + ] +} \ No newline at end of file diff --git a/Neoforge/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json b/Neoforge/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json new file mode 100644 index 000000000..3612273f7 --- /dev/null +++ b/Neoforge/src/generated/resources/data/hexcasting/recipe/dynamicerase_spellbook.json @@ -0,0 +1,4 @@ +{ + "type": "hexcasting:erase_spellbook", + "category": "misc" +} \ No newline at end of file diff --git a/Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json b/Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json new file mode 100644 index 000000000..355a91fd0 --- /dev/null +++ b/Neoforge/src/generated/resources/data/hexcasting/tags/item/spellbook_erase_materials.json @@ -0,0 +1,5 @@ +{ + "values": [ + "minecraft:soul_sand" + ] +} \ No newline at end of file From ec8552a427f1eb40dd0788c19cefadffb61859ee Mon Sep 17 00:00:00 2001 From: Robotgiggle <88736742+Robotgiggle@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:59:17 -0400 Subject: [PATCH 9/9] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76dadf05..98467f9e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Added the `hex_unbreakable` tag for blocks that should be immune to Break Block regardless of the configured mining tier ([#1186](https://github.com/FallingColors/HexMod/pull/1186)) @Robotgiggle @slava110 - Added a new Ancient Cypher hex that impulses nearby items towards the caster ([#1106](https://github.com/FallingColors/HexMod/pull/1106)) @IridescentVoid - Added a recipe to copy a spellbook's contents into another blank spellbook using dragon's breath ([#1231](https://github.com/FallingColors/HexMod/pull/1231)) @Robotgiggle +- Added a recipe to fully erase a spellbook's contents using soul sand ([#1231](https://github.com/FallingColors/HexMod/pull/1231)) @Robotgiggle ### Changed