From b240c4acbbd121ed33826c821549912dab9380cd Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 27 Mar 2026 23:19:58 +0100 Subject: [PATCH 01/10] feat: TimeSpell changes - Added "time" as alias to "time-to-set". - Added "add-time" and "set-player-time" toggles. - Removed default "str-announce" for convenience. - Implemented TargetedEntitySpell for player targeting. --- .../java/com/nisovin/magicspells/Spell.java | 16 +++++-- .../magicspells/spells/instant/TimeSpell.java | 46 +++++++++++++------ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/Spell.java b/core/src/main/java/com/nisovin/magicspells/Spell.java index f7db15374..3fd198dbf 100644 --- a/core/src/main/java/com/nisovin/magicspells/Spell.java +++ b/core/src/main/java/com/nisovin/magicspells/Spell.java @@ -1236,7 +1236,7 @@ public void sendMessages(LivingEntity caster, String[] args) { public void sendMessages(SpellData data, String... replacements) { sendMessage(strCastSelf, data.caster(), data, replacements); sendMessage(strCastTarget, data.target(), data, replacements); - sendMessageNear(strCastOthers, data, broadcastRange.get(data), replacements); + sendMessageNear(strCastOthers, data, replacements); } protected boolean preCastTimeCheck(LivingEntity livingEntity, String[] args) { @@ -2296,8 +2296,7 @@ protected void sendMessage(String message, LivingEntity recipient, SpellData dat */ @Deprecated protected void sendMessageNear(LivingEntity livingEntity, String message) { - SpellData data = new SpellData(livingEntity); - sendMessageNear(message, data, broadcastRange.get(data)); + sendMessageNear(message, new SpellData(livingEntity)); } /** @@ -2325,6 +2324,17 @@ protected void sendMessageNear(LivingEntity livingEntity, Player ignore, String sendMessageNear(message, new SpellData(livingEntity, ignore, 1f, args), range, replacements); } + /** + * Sends a message to all players near the specified player, within the default broadcast range. + * + * @param message the message to send + * @param data the associated spell data + * @param replacements replacements to be done on message + */ + protected void sendMessageNear(String message, SpellData data, String... replacements) { + sendMessageNear(message, data, broadcastRange.get(data), replacements); + } + /** * Sends a message to all players near the specified player, within the specified broadcast range. * diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java index 9f142d43c..ac361de5c 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/TimeSpell.java @@ -8,37 +8,57 @@ import com.nisovin.magicspells.util.MagicConfig; import com.nisovin.magicspells.spells.InstantSpell; import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spells.TargetedEntitySpell; import com.nisovin.magicspells.spells.TargetedLocationSpell; -public class TimeSpell extends InstantSpell implements TargetedLocationSpell { +public class TimeSpell extends InstantSpell implements TargetedEntitySpell, TargetedLocationSpell { - private final ConfigData timeToSet; + private final ConfigData time; + + private final ConfigData addTime; + private final ConfigData setPlayerTime; private String strAnnounce; - + public TimeSpell(MagicConfig config, String spellName) { super(config, spellName); - - timeToSet = getConfigDataInt("time-to-set", 0); - strAnnounce = getConfigString("str-announce", "The sun suddenly appears in the sky."); + + time = getConfigDataInt("time", getConfigDataInt("time-to-set", 0)); + + addTime = getConfigDataBoolean("add-time", false); + setPlayerTime = getConfigDataBoolean("set-player-time", false); + + strAnnounce = getConfigString("str-announce", ""); } @Override public CastResult cast(SpellData data) { - setTime(data.caster().getWorld(), data); - return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + return setTime(data.caster().getWorld(), data.target(data.caster())); + } + + @Override + public CastResult castAtEntity(SpellData data) { + return setTime(data.target().getWorld(), data); } @Override public CastResult castAtLocation(SpellData data) { - setTime(data.location().getWorld(), data); - return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + return setTime(data.location().getWorld(), data); } - private void setTime(World world, SpellData data) { - world.setTime(timeToSet.get(data)); - for (Player p : world.getPlayers()) sendMessage(strAnnounce, p, data); + private CastResult setTime(World world, SpellData data) { + long time = this.time.get(data); + if (addTime.get(data)) time += world.getTime(); + + if (setPlayerTime.get(data)) { + if (!(data.target() instanceof Player player)) return noTarget(data); + player.setPlayerTime(time, true); // Reset with "time: 0". + } else world.setTime(time); + + sendMessageNear(strAnnounce, data); playSpellEffects(data); + + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); } public String getStrAnnounce() { From 251f945733baf4abd3852f7ff0d394ff06a07543 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 20 Apr 2026 22:14:56 +0200 Subject: [PATCH 02/10] feat: PasteSpell changes Added "paste-structure-void", "prevent-overwrite", and "overwrite-replaceable". --- .../spells/targeted/PasteSpell.java | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java index 47b5965d6..e8cd454ff 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/PasteSpell.java @@ -6,7 +6,10 @@ import java.io.IOException; import java.io.FileInputStream; +import org.bukkit.World; +import org.bukkit.Material; import org.bukkit.Location; +import org.bukkit.block.Block; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.EditSession; @@ -30,7 +33,7 @@ public class PasteSpell extends TargetedSpell implements TargetedLocationSpell { - private final List sessions; + private final List sessions = new ArrayList<>(); private Clipboard clipboard; @@ -42,6 +45,9 @@ public class PasteSpell extends TargetedSpell implements TargetedLocationSpell { private final ConfigData pasteAir; private final ConfigData removePaste; private final ConfigData pasteAtCaster; + private final ConfigData preventOverwrite; + private final ConfigData pasteStructureVoid; + private final ConfigData overwriteReplaceable; public PasteSpell(MagicConfig config, String spellName) { super(config, spellName); @@ -58,8 +64,9 @@ public PasteSpell(MagicConfig config, String spellName) { pasteAir = getConfigDataBoolean("paste-air", false); removePaste = getConfigDataBoolean("remove-paste", true); pasteAtCaster = getConfigDataBoolean("paste-at-caster", false); - - sessions = new ArrayList<>(); + preventOverwrite = getConfigDataBoolean("prevent-overwrite", false); + pasteStructureVoid = getConfigDataBoolean("paste-structure-void", false); + overwriteReplaceable = getConfigDataBoolean("overwrite-replaceable", false); } @Override @@ -110,11 +117,38 @@ public CastResult castAtLocation(SpellData data) { target.add(0, yOffset.get(data), 0); data = data.location(target); - try (EditSession editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(target.getWorld()))) { + World world = target.getWorld(); + BlockVector3 pasteTo = BukkitAdapter.asBlockVector(target); + + boolean ignoreAir = !pasteAir.get(data); + boolean ignoreStructureVoid = !pasteStructureVoid.get(data); + + if (preventOverwrite.get(data)) { + BlockVector3 offset = pasteTo.subtract(clipboard.getOrigin()); + + boolean overwriteReplaceable = this.overwriteReplaceable.get(data); + for (BlockVector3 pos : clipboard.getRegion()) { + BlockVector3 worldPos = pos.add(offset); + Block origin = world.getBlockAt(worldPos.x(), worldPos.y(), worldPos.z()); + + if (origin.isEmpty()) continue; + if (overwriteReplaceable && origin.isReplaceable()) continue; + + Material place = BukkitAdapter.adapt(clipboard.getFullBlock(pos).getBlockType()); + + if (ignoreAir && place.isAir()) continue; + if (ignoreStructureVoid && place == Material.STRUCTURE_VOID) continue; + + return noTarget(data); + } + } + + try (EditSession editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(world))) { Operation operation = new ClipboardHolder(clipboard) .createPaste(editSession) - .to(BlockVector3.at(target.getX(), target.getY(), target.getZ())) - .ignoreAirBlocks(!pasteAir.get(data)) + .to(pasteTo) + .ignoreAirBlocks(ignoreAir) + .ignoreStructureVoidBlocks(ignoreStructureVoid) .build(); Operations.complete(operation); From 41d265c0fb8ebf8113e51ec21c30c4d88eec85f6 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 17 May 2026 02:04:46 +0200 Subject: [PATCH 03/10] feat: WeatherSpell --- .../spells/instant/WeatherSpell.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java new file mode 100644 index 000000000..b20302f85 --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/WeatherSpell.java @@ -0,0 +1,94 @@ +package com.nisovin.magicspells.spells.instant; + +import org.bukkit.World; +import org.bukkit.WeatherType; +import org.bukkit.entity.Player; + +import net.kyori.adventure.util.TriState; + +import com.nisovin.magicspells.util.SpellData; +import com.nisovin.magicspells.util.CastResult; +import com.nisovin.magicspells.util.MagicConfig; +import com.nisovin.magicspells.spells.InstantSpell; +import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spells.TargetedEntitySpell; +import com.nisovin.magicspells.spells.TargetedLocationSpell; + +public class WeatherSpell extends InstantSpell implements TargetedEntitySpell, TargetedLocationSpell { + + private final ConfigData rain; + private final ConfigData thunder; + + private final ConfigData durationClear; + private final ConfigData durationWeather; + private final ConfigData durationThunder; + + private final ConfigData playerWeather; + + public WeatherSpell(MagicConfig config, String spellName) { + super(config, spellName); + + rain = getConfigDataEnum("rain", TriState.class, TriState.NOT_SET); + thunder = getConfigDataEnum("thunder", TriState.class, TriState.NOT_SET); + + durationClear = getConfigDataInt("duration-clear", -1); + durationWeather = getConfigDataInt("duration-weather", -1); + durationThunder = getConfigDataInt("duration-thunder", -1); + + playerWeather = getConfigDataEnum("player-weather", PlayerWeather.class, null); + } + + @Override + public CastResult cast(SpellData data) { + return weather(data.caster().getWorld(), data.target(data.caster())); + } + + @Override + public CastResult castAtEntity(SpellData data) { + return weather(data.target().getWorld(), data); + } + + @Override + public CastResult castAtLocation(SpellData data) { + return weather(data.location().getWorld(), data); + } + + private CastResult weather(World world, SpellData data) { + PlayerWeather playerWeather = this.playerWeather.get(data); + + if (playerWeather == null) { + Boolean rain = this.rain.get(data).toBoolean(); + if (rain != null) world.setStorm(rain); + + Boolean thunder = this.thunder.get(data).toBoolean(); + if (thunder != null) world.setThundering(thunder); + + int durationWeather = this.durationWeather.get(data); + if (durationWeather >= 0) world.setWeatherDuration(durationWeather); + + int durationThunder = this.durationThunder.get(data); + if (durationThunder >= 0) world.setThunderDuration(durationThunder); + + int durationClear = this.durationClear.get(data); + if (durationClear >= 0) world.setClearWeatherDuration(durationClear); + } else { + if (!(data.target() instanceof Player player)) return noTarget(data); + + switch (playerWeather) { + case CLEAR -> player.setPlayerWeather(WeatherType.CLEAR); + case DOWNFALL -> player.setPlayerWeather(WeatherType.DOWNFALL); + case RESET -> player.resetPlayerWeather(); + } + } + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + + private enum PlayerWeather { + CLEAR, + DOWNFALL, + RESET, + } + +} From 286f75d6b4bde423ad3e9ba1997cedf0fa3becd2 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 26 Apr 2026 03:09:24 +0200 Subject: [PATCH 04/10] ci: Update actions --- .github/workflows/build.yml | 8 ++++---- .github/workflows/pr_comment.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8069adf7..340587455 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout project sources - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{github.event.pull_request.head.sha || github.sha}} @@ -45,13 +45,13 @@ jobs: run: echo "version=$(grep version gradle.properties | cut -d"=" -f2 | xargs)" >> $GITHUB_OUTPUT - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 25 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v6 - name: Run build with Gradle Wrapper run: ./gradlew "-Pversion=${{steps.version.outputs.version}}-${{steps.hash.outputs.sha_short}}" core:build @@ -59,7 +59,7 @@ jobs: - name: Upload artifact if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'Build PR Jar') id: artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: MagicSpells-${{steps.version.outputs.version}}-${{steps.hash.outputs.sha_short}} if-no-files-found: error diff --git a/.github/workflows/pr_comment.yml b/.github/workflows/pr_comment.yml index 812afdaf8..07f49e37e 100644 --- a/.github/workflows/pr_comment.yml +++ b/.github/workflows/pr_comment.yml @@ -12,7 +12,7 @@ jobs: github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v9 with: script: | const label = "Build PR Jar"; From 0992815c8ee35efee6c5a98fa68034145e8179a7 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 15 May 2026 22:36:24 +0200 Subject: [PATCH 05/10] fix: Wrong int cast for a 0.0-1.0 float argument --- .../nisovin/magicspells/variables/meta/ExperienceVariable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/nisovin/magicspells/variables/meta/ExperienceVariable.java b/core/src/main/java/com/nisovin/magicspells/variables/meta/ExperienceVariable.java index cfa295b2f..fdad98855 100644 --- a/core/src/main/java/com/nisovin/magicspells/variables/meta/ExperienceVariable.java +++ b/core/src/main/java/com/nisovin/magicspells/variables/meta/ExperienceVariable.java @@ -17,7 +17,7 @@ public double getValue(String player) { @Override public void set(String player, double amount) { Player p = Bukkit.getPlayerExact(player); - if (p != null) p.setExp((int) amount); + if (p != null) p.setExp((float) amount); } } From e1bfc3d4c1cfbce83f4682504eb2347ede91dfd2 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 15 May 2026 22:38:11 +0200 Subject: [PATCH 06/10] feat: "experience" spell effect --- .../effecttypes/ExperienceEffect.java | 50 +++++++++++++++++++ .../util/managers/SpellEffectManager.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ExperienceEffect.java diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ExperienceEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ExperienceEffect.java new file mode 100644 index 000000000..4b0e93a8a --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ExperienceEffect.java @@ -0,0 +1,50 @@ +package com.nisovin.magicspells.spelleffects.effecttypes; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.configuration.ConfigurationSection; + +import com.nisovin.magicspells.util.Name; +import com.nisovin.magicspells.util.SpellData; +import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spelleffects.SpellEffect; +import com.nisovin.magicspells.util.config.ConfigDataUtil; + +@Name("experience") +public class ExperienceEffect extends SpellEffect { + + private ConfigData level; + private ConfigData progress; + + private ConfigData reset; + + @Override + protected void loadFromConfig(ConfigurationSection config) { + level = ConfigDataUtil.getInteger(config, "level"); + progress = ConfigDataUtil.getInteger(config, "progress"); + + reset = ConfigDataUtil.getBoolean(config, "reset", false); + } + + @Override + protected Runnable playEffectEntity(Entity entity, SpellData data) { + if (!(entity instanceof Player player)) return null; + + boolean reset = this.reset.get(data); + + Integer l = this.level.get(data); + int level = l == null || reset ? player.getLevel() : l; + + Integer p = this.progress.get(data); + float progress = p == null || reset ? player.getExp() : p / 100f; + + try { + player.sendExperienceChange(progress, level); + } catch (IllegalArgumentException e) { + // debug + } + + return null; + } +} + diff --git a/core/src/main/java/com/nisovin/magicspells/util/managers/SpellEffectManager.java b/core/src/main/java/com/nisovin/magicspells/util/managers/SpellEffectManager.java index 417a57874..b693494e0 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/managers/SpellEffectManager.java +++ b/core/src/main/java/com/nisovin/magicspells/util/managers/SpellEffectManager.java @@ -80,6 +80,7 @@ private void initialize() { addSpellEffect(EffectLibLineEffect.class); addSpellEffect(EnderSignalEffect.class); addSpellEffect(EntityEffect.class); + addSpellEffect(ExperienceEffect.class); addSpellEffect(ExplosionEffect.class); addSpellEffect(FireworksEffect.class); addSpellEffect(GameTestAddMarkerEffect.class); From f5f654ac68a832ca90ce658a67b85671d4d3d694 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Thu, 21 May 2026 19:11:06 +0200 Subject: [PATCH 07/10] build: Disable default artifact This prevents a race condition where, due to our shaded jar's classifier being empty, the shaded and base jar share the same name, and sometimes the unshaded artifact overwrites the shaded one. --- core/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/build.gradle b/core/build.gradle index ab2293445..c94554d62 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -62,6 +62,10 @@ shadowJar { archiveClassifier.set("") } +jar { + enabled = false +} + generateGrammarSource { packageName = "com.nisovin.magicspells.util.grammars" arguments += ["-visitor", "-no-listener"] From eda8e84638d744516920fd30d057b63cebfc4a25 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 26 May 2026 19:18:58 +0200 Subject: [PATCH 08/10] refactor: Cleanup --- .../java/com/nisovin/magicspells/Spell.java | 5 ++ .../spells/instant/EnchantSpell.java | 57 ++++++++++++------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/Spell.java b/core/src/main/java/com/nisovin/magicspells/Spell.java index 3fd198dbf..ad8a7f2d6 100644 --- a/core/src/main/java/com/nisovin/magicspells/Spell.java +++ b/core/src/main/java/com/nisovin/magicspells/Spell.java @@ -2,6 +2,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.MustBeInvokedByOverriders; import de.slikey.effectlib.Effect; @@ -537,6 +538,7 @@ protected SpellReagents getConfigReagents(String option) { return reagents; } + @MustBeInvokedByOverriders protected void initializeVariables() { // Variable options if (varModsCast != null && !varModsCast.isEmpty()) { @@ -584,6 +586,7 @@ protected void initializeVariables() { if (reagents == null) reagents = new SpellReagents(); } + @MustBeInvokedByOverriders protected void initializeSpellEffects() { // Graphical effects effectTrackerSet = new HashSet<>(); @@ -652,6 +655,7 @@ protected void initializeSpellEffect(ConfigurationSection section, String key) { // DEBUG INFO: level 2, adding modifiers to internalname // DEBUG INFO: level 2, adding target modifiers to internalname + @MustBeInvokedByOverriders protected void initializeModifiers() { // Modifiers if (modifierStrings != null && !modifierStrings.isEmpty()) { @@ -684,6 +688,7 @@ protected void initializeModifiers() { /** * This method is called immediately after all spells have been loaded. */ + @MustBeInvokedByOverriders protected void initialize() { // Process shared cooldowns List rawSharedCooldowns = config.getList(internalKey + "shared-cooldowns", null); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java index caa2680fe..6dfa7e97f 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java @@ -18,29 +18,41 @@ public class EnchantSpell extends InstantSpell { - private final Map enchantments; + private final Map enchantments = new HashMap<>(); - private ConfigData safeEnchants; + private final ConfigData safeEnchants; public EnchantSpell(MagicConfig config, String spellName) { super(config, spellName); - enchantments = new HashMap<>(); + safeEnchants = getConfigDataBoolean("safe-enchants", true); - List enchantmentList = getConfigStringList("enchantments", null); + List enchantList = getConfigStringList("enchantments", List.of()); + if (enchantList.isEmpty()) { + MagicSpells.error("EnchantSpell '" + internalName + "' has no 'enchantments' defined!"); + return; + } - safeEnchants = getConfigDataBoolean("safe-enchants", true); + for (int i = 0; i < enchantList.size(); i++) { + String[] splits = enchantList.get(i).split(" ", 2); + Enchantment enchant = EnchantmentHandler.getEnchantment(splits[0]); + if (enchant == null) { + MagicSpells.error("EnchantSpell '" + internalName + "' has an invalid enchantment key '" + splits[0] + "' on element '#" + i + "'"); + continue; + } - if (enchantmentList != null && !enchantmentList.isEmpty()) { - for (String string : enchantmentList) { - Enchantment enchant = null; - int level = 1; - String[] str = string.split(" "); - if (str[0] != null) enchant = EnchantmentHandler.getEnchantment(str[0]); - if (str.length > 1 && str[1] != null) level = Integer.parseInt(str[1]); - if (enchant != null) enchantments.put(enchant, level); + int level = enchant.getStartLevel(); + if (splits.length > 1) { + try { + level = Integer.parseInt(splits[1]); + } catch (NumberFormatException _) { + MagicSpells.error("EnchantSpell '" + internalName + "' has an invalid enchantment level '" + splits[1] + "' on element '#" + i + "'"); + continue; + } } - } else MagicSpells.error("EnchantSpell '" + internalName + "' has invalid enchantments defined!"); + + enchantments.put(enchant, level); + } } @Override @@ -49,7 +61,7 @@ public CastResult cast(SpellData data) { if (eq == null) return new CastResult(PostCastAction.ALREADY_HANDLED, data); ItemStack item = eq.getItemInMainHand(); - if (item.getType().isAir()) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + if (item.isEmpty()) return new CastResult(PostCastAction.ALREADY_HANDLED, data); boolean safeEnchants = this.safeEnchants.get(data); for (Enchantment e : enchantments.keySet()) @@ -61,13 +73,16 @@ public CastResult cast(SpellData data) { } private void enchant(ItemStack item, boolean safeEnchants, Enchantment enchant, int level) { - if (!enchant.canEnchantItem(item)) return; - if (safeEnchants && level > enchant.getMaxLevel()) level = enchant.getMaxLevel(); - if (level <= 0) item.removeEnchantment(enchant); - else { - if (safeEnchants) item.addEnchantment(enchant, level); - else item.addUnsafeEnchantment(enchant, level); + if (level <= 0) { + item.removeEnchantment(enchant); + return; } + + if (!enchant.canEnchantItem(item)) return; + + if (safeEnchants) level = Math.clamp(level, enchant.getStartLevel(), enchant.getMaxLevel()); + + item.addUnsafeEnchantment(enchant, level); } public Map getEnchantments() { From 4c8f09a49949e81a5eb744624ca8b479be3dc67a Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 5 Jun 2026 03:26:10 +0200 Subject: [PATCH 09/10] feat: Add "require-supported-item" --- .../magicspells/spells/instant/EnchantSpell.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java index 6dfa7e97f..75eeebdc3 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/EnchantSpell.java @@ -21,11 +21,13 @@ public class EnchantSpell extends InstantSpell { private final Map enchantments = new HashMap<>(); private final ConfigData safeEnchants; + private final ConfigData requireSupportedItem; public EnchantSpell(MagicConfig config, String spellName) { super(config, spellName); safeEnchants = getConfigDataBoolean("safe-enchants", true); + requireSupportedItem = getConfigDataBoolean("require-supported-item", true); List enchantList = getConfigStringList("enchantments", List.of()); if (enchantList.isEmpty()) { @@ -64,22 +66,23 @@ public CastResult cast(SpellData data) { if (item.isEmpty()) return new CastResult(PostCastAction.ALREADY_HANDLED, data); boolean safeEnchants = this.safeEnchants.get(data); + boolean requireSupportedItem = this.requireSupportedItem.get(data); + for (Enchantment e : enchantments.keySet()) - enchant(item, safeEnchants, e, enchantments.get(e)); + enchant(item, safeEnchants, requireSupportedItem, e, enchantments.get(e)); playSpellEffects(data); return new CastResult(PostCastAction.HANDLE_NORMALLY, data); } - private void enchant(ItemStack item, boolean safeEnchants, Enchantment enchant, int level) { + private void enchant(ItemStack item, boolean safeEnchants, boolean requireSupportedItem, Enchantment enchant, int level) { if (level <= 0) { item.removeEnchantment(enchant); return; } - if (!enchant.canEnchantItem(item)) return; - + if (requireSupportedItem && !enchant.canEnchantItem(item)) return; if (safeEnchants) level = Math.clamp(level, enchant.getStartLevel(), enchant.getMaxLevel()); item.addUnsafeEnchantment(enchant, level); From cac26326799f0c380f28b8f6491680b45c2e572c Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 5 Jun 2026 17:46:43 +0200 Subject: [PATCH 10/10] chore: Bump EvalEx --- core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/build.gradle b/core/build.gradle index c94554d62..63f5d26b6 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -11,7 +11,7 @@ dependencies { shadow("org.incendo:cloud-minecraft-extras:2.0.0-beta.15") shadow("org.incendo:cloud-processors-requirements:1.0.0-rc.1") shadow("org.bstats:bstats-bukkit:3.2.1") - shadow("com.github.ezylang:EvalEx:3.6.0") + shadow("com.github.ezylang:EvalEx:3.6.2") shadow("org.antlr:antlr4-runtime:4.13.2") antlr("org.antlr:antlr4:4.13.2")