From b240c4acbbd121ed33826c821549912dab9380cd Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 27 Mar 2026 23:19:58 +0100 Subject: [PATCH 01/13] 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 d358ab4c008ba4478144f57ddf030fd85c67e608 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 20 Apr 2026 22:14:56 +0200 Subject: [PATCH 02/13] feat: PasteSpell changes Add "paste-structure-void" and "prevent-overwrite". --- .../spells/targeted/PasteSpell.java | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 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..c3b831fd3 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 @@ -5,8 +5,15 @@ import java.util.ArrayList; import java.io.IOException; import java.io.FileInputStream; +import java.util.function.Predicate; +import org.bukkit.World; +import org.bukkit.Material; import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockType; + +import io.papermc.paper.registry.RegistryKey; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.EditSession; @@ -30,7 +37,7 @@ public class PasteSpell extends TargetedSpell implements TargetedLocationSpell { - private final List sessions; + private final List sessions = new ArrayList<>(); private Clipboard clipboard; @@ -42,6 +49,9 @@ public class PasteSpell extends TargetedSpell implements TargetedLocationSpell { private final ConfigData pasteAir; private final ConfigData removePaste; private final ConfigData pasteAtCaster; + private final ConfigData pasteStructureVoid; + + private final Predicate preventOverwrite; public PasteSpell(MagicConfig config, String spellName) { super(config, spellName); @@ -58,8 +68,11 @@ public PasteSpell(MagicConfig config, String spellName) { pasteAir = getConfigDataBoolean("paste-air", false); removePaste = getConfigDataBoolean("remove-paste", true); pasteAtCaster = getConfigDataBoolean("paste-at-caster", false); + pasteStructureVoid = getConfigDataBoolean("paste-structure-void", false); - sessions = new ArrayList<>(); + if (config.isBoolean(internalKey + "prevent-overwrite")) { + preventOverwrite = getConfigBoolean("prevent-overwrite", false) ? _ -> true : null; + } else preventOverwrite = getConfigRegistryEntryPredicate("prevent-overwrite", RegistryKey.BLOCK); } @Override @@ -110,11 +123,37 @@ 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 != null) { + BlockVector3 offset = pasteTo.subtract(clipboard.getOrigin()); + + 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 (!preventOverwrite.test(origin.getType().asBlockType())) 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 cfc011a317d1beba1467bdd6074b8cc589262e03 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 17 May 2026 02:04:46 +0200 Subject: [PATCH 03/13] 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 0469bd54bcfde49e4d2d57ae413dfd7a448e290b Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 26 Apr 2026 03:09:24 +0200 Subject: [PATCH 04/13] 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 b57dd21ff6811a21190eae08a18af8bd4722e965 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 15 May 2026 22:36:24 +0200 Subject: [PATCH 05/13] 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 e26f76615ed3a6ec115a29ec7248eb66760c34ab Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 15 May 2026 22:38:11 +0200 Subject: [PATCH 06/13] 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 a2617cb2c730bb542e743837540fa8a102b3019e Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Thu, 21 May 2026 19:11:06 +0200 Subject: [PATCH 07/13] 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 3762e7d2d25cffbd51928830e90035aa2f54d52c Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 26 May 2026 19:18:58 +0200 Subject: [PATCH 08/13] 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 c49a64eeb05224da6e225566f702a1282d795165 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 5 Jun 2026 03:26:10 +0200 Subject: [PATCH 09/13] feat: Add "require-supported-item" --- .../magicspells/spells/instant/EnchantSpell.java | 12 ++++++++---- 1 file changed, 8 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..0f07400b7 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 @@ -4,6 +4,7 @@ import java.util.List; import java.util.HashMap; +import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.EntityEquipment; @@ -21,11 +22,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 +67,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 && item.getType() != Material.BOOK && !enchant.canEnchantItem(item)) return; if (safeEnchants) level = Math.clamp(level, enchant.getStartLevel(), enchant.getMaxLevel()); item.addUnsafeEnchantment(enchant, level); From 8f68e7da176394d414b83771a506109a742cd371 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 5 Jun 2026 17:46:43 +0200 Subject: [PATCH 10/13] 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") From fdca8cb2ca40c3c5998690996a20512c9cff2536 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Wed, 17 Jun 2026 04:34:33 +0200 Subject: [PATCH 11/13] feat: Add 26.2 support --- core/build.gradle | 5 +- .../conditions/AttributeDefaultCondition.java | 8 +- .../spells/targeted/LightningSpell.java | 14 +- nms/latest/build.gradle | 2 +- .../latest/VolatileCodeLatest.java | 7 +- .../latest/VolatileGlowManagerLatest.java | 16 +- nms/v26_1_2/build.gradle | 8 + .../v26_1_2/VolatileCode_v26_1_2.java | 294 ++++++++++++++++ .../v26_1_2/VolatileGlowManager_v26_1_2.java | 321 ++++++++++++++++++ settings.gradle.kts | 1 + shop/build.gradle | 2 +- 11 files changed, 649 insertions(+), 29 deletions(-) create mode 100644 nms/v26_1_2/build.gradle create mode 100644 nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileCode_v26_1_2.java create mode 100644 nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileGlowManager_v26_1_2.java diff --git a/core/build.gradle b/core/build.gradle index 63f5d26b6..0f291f3a2 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -7,8 +7,8 @@ dependencies { shadow("org.apache.commons:commons-math4-core:4.0-beta1") shadow("com.github.ben-manes.caffeine:caffeine:3.2.2") shadow("com.github.Chronoken:EffectLib:4e37625") - shadow("org.incendo:cloud-paper:2.0.0-beta.14") - shadow("org.incendo:cloud-minecraft-extras:2.0.0-beta.15") + shadow("org.incendo:cloud-paper:2.0.0-beta.16") + shadow("org.incendo:cloud-minecraft-extras:2.0.0-beta.16") 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.2") @@ -18,6 +18,7 @@ dependencies { shadow(project(path: ":nms:shared", configuration: "apiElements")) shadow(project(path: ":nms:latest")) { transitive = false } + shadow(project(path: ":nms:v26_1_2")) { transitive = false } implementation("com.github.retrooper:packetevents-spigot:2.11.2") implementation("net.dmulloy2:ProtocolLib:5.4.0") { transitive = false } diff --git a/core/src/main/java/com/nisovin/magicspells/castmodifiers/conditions/AttributeDefaultCondition.java b/core/src/main/java/com/nisovin/magicspells/castmodifiers/conditions/AttributeDefaultCondition.java index e975f3cf1..ed0194664 100644 --- a/core/src/main/java/com/nisovin/magicspells/castmodifiers/conditions/AttributeDefaultCondition.java +++ b/core/src/main/java/com/nisovin/magicspells/castmodifiers/conditions/AttributeDefaultCondition.java @@ -6,6 +6,7 @@ import java.util.regex.Pattern; import org.bukkit.Location; +import org.bukkit.entity.EntityType; import org.bukkit.attribute.Attribute; import org.bukkit.entity.LivingEntity; import org.bukkit.attribute.AttributeInstance; @@ -48,10 +49,13 @@ public boolean initialize(@NotNull String var) { @Override public boolean check(LivingEntity caster) { - AttributeInstance instance = caster.getAttribute(attribute); + EntityType entityType = caster.getType(); + if (!entityType.hasDefaultAttributes()) return false; + + AttributeInstance instance = entityType.getDefaultAttributes().getAttribute(attribute); if (instance == null) return false; - return compare(instance.getDefaultValue(), value); + return compare(instance.getBaseValue(), value); } @Override diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/LightningSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/LightningSpell.java index 1107fe4c0..a63ca10ec 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/LightningSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/LightningSpell.java @@ -6,12 +6,12 @@ import org.bukkit.Location; import org.bukkit.event.Listener; +import org.bukkit.entity.EntityType; import org.bukkit.damage.DamageType; import org.bukkit.event.EventHandler; import org.bukkit.entity.LivingEntity; import org.bukkit.damage.DamageSource; import org.bukkit.entity.LightningStrike; -import org.bukkit.event.entity.PigZapEvent; import org.bukkit.event.entity.CreeperPowerEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; @@ -154,18 +154,12 @@ public void onCreeperCharge(CreeperPowerEvent event) { event.setCancelled(true); } - @EventHandler - public void onPigZap(PigZapEvent event) { - ChargeOption option = striking.get(event.getLightning().getUniqueId()); - if (option == null || option.transformEntities && option.changePig) return; - - event.setCancelled(true); - } - @EventHandler public void onZap(EntityZapEvent event) { ChargeOption option = striking.get(event.getBolt().getUniqueId()); - if (option == null || option.transformEntities) return; + + if (option == null) return; + if (option.transformEntities && (event.getEntity().getType() != EntityType.PIG || option.changePig)) return; event.setCancelled(true); } diff --git a/nms/latest/build.gradle b/nms/latest/build.gradle index 78c915976..47d830cfc 100644 --- a/nms/latest/build.gradle +++ b/nms/latest/build.gradle @@ -3,6 +3,6 @@ plugins { } dependencies { - paperweight.paperDevBundle("26.1.2.build.+") + paperweight.paperDevBundle("26.2.build.+") implementation project(":nms:shared") } diff --git a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java index 8034727e9..a222318c7 100644 --- a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java +++ b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java @@ -48,17 +48,18 @@ import net.minecraft.world.phys.Vec3; import net.minecraft.resources.Identifier; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.entity.EntityType; import net.minecraft.network.protocol.game.*; +import net.minecraft.world.entity.EntityTypes; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.item.PrimedTnt; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.advancements.triggers.Criterion; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.core.particles.ColorParticleOption; -import net.minecraft.advancements.criterion.ImpossibleTrigger; +import net.minecraft.advancements.triggers.ImpossibleTrigger; import net.minecraft.world.entity.boss.enderdragon.EnderDragon; public class VolatileCodeLatest extends VolatileCodeHandle { @@ -153,7 +154,7 @@ public boolean simulateTnt(Location target, LivingEntity source, float explosion @Override public void playDragonDeathEffect(Location location) { - EnderDragon dragon = new EnderDragon(EntityType.ENDER_DRAGON, ((CraftWorld) location.getWorld()).getHandle()); + EnderDragon dragon = new EnderDragon(EntityTypes.ENDER_DRAGON, ((CraftWorld) location.getWorld()).getHandle()); dragon.setPos(location.x(), location.y(), location.z()); BlockPos pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); diff --git a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java index b70df77e6..a59ef0817 100644 --- a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java +++ b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileGlowManagerLatest.java @@ -12,7 +12,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; -import net.minecraft.ChatFormatting; +import net.minecraft.world.scores.TeamColor; import net.minecraft.network.protocol.Packet; import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; @@ -92,14 +92,12 @@ protected Collection createAddTeamPackets() { Visibility visibility = getStringOption("name-tag-visibility", Visibility.ALWAYS, StringRepresentable.createNameLookup(Visibility.values()), config, helper::error); Scoreboard scoreboard = new Scoreboard(); - for (ChatFormatting formatting : ChatFormatting.values()) { - if (!formatting.isColor()) continue; - - PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + formatting.getName()); + for (TeamColor color : TeamColor.values()) { + PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + color.getSerializedName()); team.setSeeFriendlyInvisibles(seeFriendlyInvisibles); team.setNameTagVisibility(visibility); team.setCollisionRule(collision); - team.setColor(formatting); + team.setColor(Optional.of(color)); teamPackets.add(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)); } @@ -112,10 +110,8 @@ protected Collection createRemoveTeamPackets() { List packets = new ArrayList<>(); Scoreboard scoreboard = new Scoreboard(); - for (ChatFormatting formatting : ChatFormatting.values()) { - if (!formatting.isColor()) continue; - - PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + formatting.getName()); + for (TeamColor color : TeamColor.values()) { + PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + color.getSerializedName()); packets.add(ClientboundSetPlayerTeamPacket.createRemovePacket(team)); } diff --git a/nms/v26_1_2/build.gradle b/nms/v26_1_2/build.gradle new file mode 100644 index 000000000..78c915976 --- /dev/null +++ b/nms/v26_1_2/build.gradle @@ -0,0 +1,8 @@ +plugins { + id "io.papermc.paperweight.userdev" +} + +dependencies { + paperweight.paperDevBundle("26.1.2.build.+") + implementation project(":nms:shared") +} diff --git a/nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileCode_v26_1_2.java b/nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileCode_v26_1_2.java new file mode 100644 index 000000000..a877d444e --- /dev/null +++ b/nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileCode_v26_1_2.java @@ -0,0 +1,294 @@ +package com.nisovin.magicspells.volatilecode.v26_1_2; + +import java.util.*; +import java.lang.invoke.VarHandle; +import java.util.function.Consumer; +import java.lang.invoke.MethodType; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +import org.bukkit.World; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.util.Vector; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.entity.LivingEntity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.event.entity.ExplosionPrimeEvent; + +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.entity.CraftEntity; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.craftbukkit.entity.CraftTNTPrimed; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.entity.CraftLivingEntity; + +import org.jetbrains.annotations.NotNull; + +import net.kyori.adventure.text.Component; + +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.advancement.AdvancementDisplay; +import io.papermc.paper.threadedregions.EntityScheduler; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; + +import com.nisovin.magicspells.util.glow.GlowManager; +import com.nisovin.magicspells.volatilecode.VolatileCodeHandle; +import com.nisovin.magicspells.volatilecode.VolatileCodeHelper; + +import net.minecraft.util.ARGB; +import net.minecraft.core.BlockPos; +import net.minecraft.advancements.*; +import net.minecraft.world.phys.Vec3; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.EntityType; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.core.particles.ColorParticleOption; +import net.minecraft.advancements.criterion.ImpossibleTrigger; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; + +public class VolatileCode_v26_1_2 extends VolatileCodeHandle { + + private final Identifier TOAST_KEY = Identifier.fromNamespaceAndPath("magicspells", "toast_effect"); + + private final EntityDataAccessor> DATA_EFFECT_PARTICLES; + private final EntityDataAccessor DATA_EFFECT_AMBIENCE_ID; + private final EntityDataAccessor DATA_SHARED_FLAGS_ID; + private final MethodHandle UPDATE_EFFECT_PARTICLES; + + private final Long2ObjectOpenHashMap> GLOBAL_REGION_TASKS; + private final VarHandle CURRENTLY_EXECUTING_HANDLE; + private final VarHandle ONE_TIME_DELAYED_HANDLE; + private final VarHandle RUN_HANDLE; + + @SuppressWarnings("unchecked") + public VolatileCode_v26_1_2(VolatileCodeHelper helper) throws Exception { + super(helper); + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + + Class leClass = net.minecraft.world.entity.LivingEntity.class; + Class eClass = net.minecraft.world.entity.Entity.class; + + DATA_SHARED_FLAGS_ID = (EntityDataAccessor<@NotNull Byte>) MethodHandles.privateLookupIn(eClass, lookup) + .findStaticVarHandle(eClass, "DATA_SHARED_FLAGS_ID", EntityDataAccessor.class).get(); + + MethodHandles.Lookup leLookup = MethodHandles.privateLookupIn(leClass, lookup); + + DATA_EFFECT_PARTICLES = (EntityDataAccessor<@NotNull List>) leLookup + .findStaticVarHandle(leClass, "DATA_EFFECT_PARTICLES", EntityDataAccessor.class).get(); + + DATA_EFFECT_AMBIENCE_ID = (EntityDataAccessor<@NotNull Boolean>) leLookup + .findStaticVarHandle(leClass, "DATA_EFFECT_AMBIENCE_ID", EntityDataAccessor.class).get(); + + UPDATE_EFFECT_PARTICLES = leLookup.findVirtual(leClass, "updateSynchronizedMobEffectParticles", MethodType.methodType(void.class)); + + GLOBAL_REGION_TASKS = (Long2ObjectOpenHashMap>) MethodHandles.privateLookupIn(FoliaGlobalRegionScheduler.class, lookup) + .findVarHandle(FoliaGlobalRegionScheduler.class, "tasksByDeadline", Long2ObjectOpenHashMap.class) + .get(Bukkit.getGlobalRegionScheduler()); + + MethodHandles.Lookup esLookup = MethodHandles.privateLookupIn(EntityScheduler.class, lookup); + + CURRENTLY_EXECUTING_HANDLE = esLookup.findVarHandle(EntityScheduler.class, "currentlyExecuting", ArrayDeque.class); + ONE_TIME_DELAYED_HANDLE = esLookup.findVarHandle(EntityScheduler.class, "oneTimeDelayed", Long2ObjectOpenHashMap.class); + + Class scheduledTaskClass = esLookup.findClass("io.papermc.paper.threadedregions.EntityScheduler$ScheduledTask"); + RUN_HANDLE = esLookup.findVarHandle(scheduledTaskClass, "run", Consumer.class); + } + + @Override + public void addPotionGraphicalEffect(LivingEntity entity, int color, long duration) { + var nmsEntity = (((CraftLivingEntity) entity)).getHandle(); + SynchedEntityData entityData = nmsEntity.getEntityData(); + + entityData.set( + DATA_EFFECT_PARTICLES, + List.of(ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, ARGB.opaque(color))) + ); + + entityData.set(DATA_EFFECT_AMBIENCE_ID, false); + + if (duration <= 0) return; + helper.scheduleDelayedTask(() -> { + try { + UPDATE_EFFECT_PARTICLES.invoke(nmsEntity); + } catch (Throwable e) { + e.printStackTrace(); + } + }, duration); + } + + @Override + public void sendFakeSlotUpdate(Player player, int slot, ItemStack item) { + var nmsItem = CraftItemStack.asNMSCopy(item); + ClientboundContainerSetSlotPacket packet = new ClientboundContainerSetSlotPacket(0, 0, slot + 36, nmsItem); + + ((CraftPlayer) player).getHandle().connection.send(packet); + } + + @Override + public boolean simulateTnt(Location target, LivingEntity source, float explosionSize, boolean fire) { + ServerLevel world = ((CraftWorld) target.getWorld()).getHandle(); + var igniter = ((CraftLivingEntity) source).getHandle(); + + PrimedTnt tnt = new PrimedTnt(world, target.x(), target.y(), target.z(), igniter); + CraftTNTPrimed craftTNT = new CraftTNTPrimed((CraftServer) Bukkit.getServer(), tnt); + + return !new ExplosionPrimeEvent(craftTNT, explosionSize, fire).callEvent(); + } + + @Override + public void playDragonDeathEffect(Location location) { + EnderDragon dragon = new EnderDragon(EntityType.ENDER_DRAGON, ((CraftWorld) location.getWorld()).getHandle()); + dragon.setPos(location.x(), location.y(), location.z()); + + BlockPos pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + ClientboundAddEntityPacket addMobPacket = new ClientboundAddEntityPacket(dragon, 0, pos); + ClientboundEntityEventPacket entityEventPacket = new ClientboundEntityEventPacket(dragon, (byte) 3); + ClientboundRemoveEntitiesPacket removeEntityPacket = new ClientboundRemoveEntitiesPacket(dragon.getId()); + + List players = new ArrayList<>(); + for (Player player : location.getNearbyPlayers(64.0)) { + players.add(player); + ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle(); + nmsPlayer.connection.send(addMobPacket); + nmsPlayer.connection.send(entityEventPacket); + } + + helper.scheduleDelayedTask(() -> { + for (Player player : players) { + if (!player.isValid()) continue; + ((CraftPlayer) player).getHandle().connection.send(removeEntityPacket); + } + }, 200); + } + + @Override + public void setClientVelocity(Player player, Vector velocity) { + Vec3 pos = new Vec3(velocity.getX(), velocity.getY(), velocity.getZ()); + ClientboundSetEntityMotionPacket packet = new ClientboundSetEntityMotionPacket(player.getEntityId(), pos); + ((CraftPlayer) player).getHandle().connection.send(packet); + } + + @Override + public void playHurtSound(LivingEntity entity) { + var nmsEntity = ((CraftLivingEntity) entity).getHandle(); + var sound = nmsEntity.getHurtSound(nmsEntity.damageSources().generic()); + + if (sound == null || nmsEntity.isSilent()) return; + nmsEntity.level().playSound( + null, + nmsEntity.blockPosition(), + sound, + nmsEntity.getSoundSource(), + nmsEntity.getSoundVolume(), + nmsEntity.getVoicePitch() + ); + } + + @Override + public void sendToastEffect(Player receiver, ItemStack icon, AdvancementDisplay.Frame frameType, Component text) { + var iconNms = CraftItemStack.asNMSCopy(icon).getItem(); + var textNms = PaperAdventure.asVanilla(text); + var description = PaperAdventure.asVanilla(Component.empty()); + AdvancementType frame; + try { + frame = AdvancementType.valueOf(frameType.name()); + } catch (IllegalArgumentException ignored) { + frame = AdvancementType.TASK; + } + + AdvancementHolder advancement = Advancement.Builder.advancement() + .display(iconNms, textNms, description, null, frame, true, false, true) + .addCriterion("impossible", new Criterion<>(new ImpossibleTrigger(), new ImpossibleTrigger.TriggerInstance())) + .build(TOAST_KEY); + AdvancementProgress progress = new AdvancementProgress(); + progress.update(new AdvancementRequirements(List.of(List.of("impossible")))); + progress.grantProgress("impossible"); + + ServerPlayer player = ((CraftPlayer) receiver).getHandle(); + player.connection.send(new ClientboundUpdateAdvancementsPacket( + false, + Collections.singleton(advancement), + Collections.emptySet(), + Collections.singletonMap(TOAST_KEY, progress), + true + )); + player.connection.send(new ClientboundUpdateAdvancementsPacket( + false, + Collections.emptySet(), + Collections.singleton(TOAST_KEY), + Collections.emptyMap(), + true + )); + } + + @Override + public byte getEntityMetadata(Entity entity) { + return ((CraftEntity) entity).getHandle().getEntityData().get(DATA_SHARED_FLAGS_ID); + } + + @Override + public Entity getEntityFromId(World world, int id) { + var entity = ((CraftWorld) world).getHandle().moonrise$getEntityLookup().get(id); + return entity == null ? null : entity.getBukkitEntity(); + } + + @Override + public GlowManager getGlowManager() { + return new VolatileGlowManager_v26_1_2(helper); + } + + @Override + public long countGlobalRegionSchedulerTasks() { + Plugin plugin = helper.getPlugin(); + + return GLOBAL_REGION_TASKS.values().stream() + .flatMap(List::stream) + .filter(task -> task.getOwningPlugin() == plugin) + .count(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public long countEntitySchedulerTasks() { + EntityScheduler.EntitySchedulerTickList entitySchedulerTickList = MinecraftServer.getServer().entitySchedulerTickList; + EntityScheduler[] schedulers = entitySchedulerTickList.getAllSchedulers(); + Plugin plugin = helper.getPlugin(); + + int count = 0; + for (EntityScheduler scheduler : schedulers) { + Long2ObjectOpenHashMap oneTimeDelayed = (Long2ObjectOpenHashMap) ONE_TIME_DELAYED_HANDLE.get(scheduler); + ArrayDeque currentlyExecuting = (ArrayDeque) CURRENTLY_EXECUTING_HANDLE.get(scheduler); + + for (List taskList : oneTimeDelayed.values()) { + for (Object taskObject : taskList) { + ScheduledTask task = (ScheduledTask) (Consumer) RUN_HANDLE.get(taskObject); + if (task.getOwningPlugin() == plugin) count++; + } + } + + for (Object taskObject : currentlyExecuting) { + ScheduledTask task = (ScheduledTask) (Consumer) RUN_HANDLE.get(taskObject); + if (task.getOwningPlugin() == plugin) count++; + } + } + + return count; + } + +} diff --git a/nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileGlowManager_v26_1_2.java b/nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileGlowManager_v26_1_2.java new file mode 100644 index 000000000..44f290f97 --- /dev/null +++ b/nms/v26_1_2/src/main/java/com/nisovin/magicspells/volatilecode/v26_1_2/VolatileGlowManager_v26_1_2.java @@ -0,0 +1,321 @@ +package com.nisovin.magicspells.volatilecode.v26_1_2; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.lang.invoke.MethodType; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; + +import io.netty.channel.ChannelPromise; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.protocol.Packet; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.scores.Team.Visibility; +import net.minecraft.world.scores.Team.CollisionRule; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.scoreboard.Team; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.craftbukkit.entity.CraftEntity; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.configuration.ConfigurationSection; + +import com.nisovin.magicspells.util.glow.LibsDisguiseHelper; +import com.nisovin.magicspells.volatilecode.VolatileCodeHelper; +import com.nisovin.magicspells.util.glow.PacketBasedGlowManager; + +public class VolatileGlowManager_v26_1_2 extends PacketBasedGlowManager, ClientboundSetEntityDataPacket, ClientboundSetPlayerTeamPacket> { + + private static final EntityDataAccessor DATA_SHARED_FLAGS_ID = new EntityDataAccessor<>(0, EntityDataSerializers.BYTE); + + private final Set> handled = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>())); + private final MethodHandle teamPacketHandle; + private final VolatileCodeHelper helper; + + public VolatileGlowManager_v26_1_2(VolatileCodeHelper helper) { + this.helper = helper; + + try { + teamPacketHandle = MethodHandles + .privateLookupIn(ClientboundSetPlayerTeamPacket.class, MethodHandles.lookup()) + .findConstructor( + ClientboundSetPlayerTeamPacket.class, + MethodType.methodType(void.class, String.class, int.class, Optional.class, Collection.class) + ); + } catch (Exception e) { + throw new RuntimeException("Encountered an error while initializing VolatileGlowManagerLatest", e); + } + + helper.registerEvents(this); + } + + @Override + public void load() { + super.load(); + + Bukkit.getOnlinePlayers().forEach(this::addGlowChannelHandler); + } + + @Override + public synchronized void unload() { + Bukkit.getOnlinePlayers().forEach(this::removeGlowChannelHandler); + + super.unload(); + handled.clear(); + } + + @Override + protected Collection createAddTeamPackets() { + List teamPackets = new ArrayList<>(); + + ConfigurationSection config = helper.getMainConfig(); + boolean seeFriendlyInvisibles = config.getBoolean("general.glow-spell-scoreboard-teams.see-friendly-invisibles", false); + CollisionRule collision = getStringOption("collision-rule", CollisionRule.ALWAYS, StringRepresentable.createNameLookup(CollisionRule.values()), config, helper::error); + Visibility visibility = getStringOption("name-tag-visibility", Visibility.ALWAYS, StringRepresentable.createNameLookup(Visibility.values()), config, helper::error); + + Scoreboard scoreboard = new Scoreboard(); + for (ChatFormatting formatting : ChatFormatting.values()) { + if (!formatting.isColor()) continue; + + PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + formatting.getName()); + team.setSeeFriendlyInvisibles(seeFriendlyInvisibles); + team.setNameTagVisibility(visibility); + team.setCollisionRule(collision); + team.setColor(formatting); + + teamPackets.add(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, true)); + } + + return teamPackets; + } + + @Override + protected Collection createRemoveTeamPackets() { + List packets = new ArrayList<>(); + + Scoreboard scoreboard = new Scoreboard(); + for (ChatFormatting formatting : ChatFormatting.values()) { + if (!formatting.isColor()) continue; + + PlayerTeam team = new PlayerTeam(scoreboard, "magicspells:" + formatting.getName()); + packets.add(ClientboundSetPlayerTeamPacket.createRemovePacket(team)); + } + + return packets; + } + + @Override + protected ClientboundSetEntityDataPacket createEntityDataPacket(@NotNull Entity entity, boolean forceGlow) { + byte metadata = ((CraftEntity) entity).getHandle().getEntityData().get(DATA_SHARED_FLAGS_ID); + if (forceGlow) metadata |= 0x40; + + return new ClientboundSetEntityDataPacket( + entity.getEntityId(), + list(new SynchedEntityData.DataValue<>( + 0, + EntityDataSerializers.BYTE, + metadata + )) + ); + } + + @Override + protected Collection createJoinTeamPacket(@NotNull GlowData data) { + return data.lastScoreboardEntry().map(entry -> { + try { + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + "magicspells:" + data.color(), + 3, + Optional.empty(), + list(entry) + ); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + + @Override + protected Collection createResetTeamPacket(org.bukkit.scoreboard.@NotNull Scoreboard scoreboard, @NotNull GlowData data) { + return data.lastScoreboardEntry().map(entry -> { + try { + Team team = scoreboard.getEntryTeam(entry); + if (team != null) { + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + team.getName(), + 3, + Optional.empty(), + list(entry) + ); + } + + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + "magicspells:" + data.color(), + 4, + Optional.empty(), + list(entry) + ); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + + @Override + protected void sendPacket(@NotNull Player player, @NotNull Packet packet) { + if (!(packet instanceof ClientboundSetPlayerTeamPacket teamPacket) || teamPacket.getTeamAction() == null) + handled.add(packet); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + connection.send(packet); + } + + @Override + protected void registerEvents(Listener listener) { + helper.registerEvents(listener); + } + + @Override + protected void cancelTask(int taskId) { + helper.cancelTask(taskId); + } + + @Override + public int scheduleDelayedTask(Runnable runnable, long delay) { + return helper.scheduleDelayedTask(runnable, delay); + } + + private void addGlowChannelHandler(Player player) { + ChannelPipeline pipeline = ((CraftPlayer) player).getHandle().connection.connection.channel.pipeline(); + pipeline.addBefore("unbundler", "magicspells:glow_channel_handler", new GlowChannelHandler(player)); + } + + private void removeGlowChannelHandler(Player player) { + ChannelPipeline pipeline = ((CraftPlayer) player).getHandle().connection.connection.channel.pipeline(); + + if (pipeline.get("magicspells:glow_channel_handler") != null) + pipeline.remove("magicspells:glow_channel_handler"); + } + + private List list(T element) { + List list = new ArrayList<>(1); + list.add(element); + + return list; + } + + @Override + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + super.onPlayerJoin(event); + addGlowChannelHandler(event.getPlayer()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + removeGlowChannelHandler(event.getPlayer()); + } + + private class GlowChannelHandler extends ChannelOutboundHandlerAdapter { + + private final Player player; + + private GlowChannelHandler(Player player) { + this.player = player; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof Packet p && handled.contains(p)) { + super.write(ctx, msg, promise); + return; + } + + synchronized (VolatileGlowManager_v26_1_2.this) { + if (glows.isEmpty() && perPlayerGlows.isEmpty()) { + super.write(ctx, msg, promise); + return; + } + } + + switch (msg) { + case ClientboundSetEntityDataPacket packet -> handleEntityData(packet); + case ClientboundSetPlayerTeamPacket packet -> { + msg = handleTeamPacket(packet); + if (msg == null) return; + } + default -> {} + } + + super.write(ctx, msg, promise); + } + + private void handleEntityData(ClientboundSetEntityDataPacket packet) { + List> packedItems = packet.packedItems(); + if (packedItems.isEmpty()) return; + + SynchedEntityData.DataValue item = packedItems.getFirst(); + if (item.id() != 0) return; + + byte flags = (byte) item.value(); + if ((flags & 0x40) > 0) return; + + UUID uuid; + if (!libsDisguisesLoaded || packet.id() != LibsDisguiseHelper.getSelfDisguiseId()) { + var entity = ((CraftPlayer) player).getHandle().level().moonrise$getEntityLookup().get(packet.id()); + if (entity == null) return; + + uuid = entity.getUUID(); + } else uuid = player.getUniqueId(); + + GlowData data = getGlowData(player.getUniqueId(), uuid); + if (data == null) return; + + flags |= 0x40; + packedItems.set(0, new SynchedEntityData.DataValue<>(0, EntityDataSerializers.BYTE, flags)); + } + + private ClientboundSetPlayerTeamPacket handleTeamPacket(ClientboundSetPlayerTeamPacket packet) { + ClientboundSetPlayerTeamPacket.Action playerAction = packet.getPlayerAction(); + if (playerAction == null || packet.getTeamAction() != null) return packet; + + Collection entries = packet.getPlayers(); + if (entries.isEmpty()) return packet; + + Collection filtered = filterTeamEntries(player, entries); + if (filtered == null) return packet; + if (filtered.isEmpty()) return null; + + try { + return (ClientboundSetPlayerTeamPacket) teamPacketHandle.invoke( + packet.getName(), + playerAction == ClientboundSetPlayerTeamPacket.Action.ADD ? 3 : 4, + packet.getParameters(), + filtered + ); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d86ea860..da0f9c09a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ include("towny") include(":nms:shared") include(":nms:latest") +include(":nms:v26_1_2") pluginManagement { repositories { diff --git a/shop/build.gradle b/shop/build.gradle index ded17733e..cffaddf8b 100644 --- a/shop/build.gradle +++ b/shop/build.gradle @@ -1,6 +1,6 @@ dependencies { implementation project(":core") - implementation("org.incendo:cloud-paper:2.0.0-beta.14") + implementation("org.incendo:cloud-paper:2.0.0-beta.16") implementation("net.milkbowl.vault:VaultAPI:1.7") { transitive = false } } From c0bae64972f1789ac81f27fd797022bdbcd53eb8 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 15 Jun 2026 03:11:49 +0200 Subject: [PATCH 12/13] feat: Add missing particle data --- .../effecttypes/ParticlesEffect.java | 60 ++++++++++++------- .../spells/targeted/ParticleCloudSpell.java | 41 ++++++++++++- .../latest/VolatileCodeLatest.java | 29 +++++++++ .../volatilecode/VolatileCodeHandle.java | 17 +++++- 4 files changed, 123 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ParticlesEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ParticlesEffect.java index 5daf5d66c..f1b866a2e 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ParticlesEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ParticlesEffect.java @@ -20,6 +20,7 @@ import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.Name; +import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.config.ConfigData; import com.nisovin.magicspells.spelleffects.SpellEffect; @@ -32,41 +33,46 @@ public class ParticlesEffect extends SpellEffect { protected ConfigData particle; - protected ConfigData rgbColor; + private ConfigData rgbColor; protected ConfigData argbColor; - protected ConfigData material; - protected ConfigData blockData; - protected ConfigData dustOptions; - protected ConfigData spellOptions; - protected ConfigData dustTransition; + private ConfigData material; + private ConfigData blockData; + private ConfigData dustOptions; + private ConfigData spellOptions; + private ConfigData dustTransition; - protected ConfigData vibrationOffset; - protected ConfigData vibrationRelativeOffset; - protected ConfigData vibrationOrigin; - protected ConfigData vibrationDestination; + private ConfigData vibrationOffset; + private ConfigData vibrationRelativeOffset; + private ConfigData vibrationOrigin; + private ConfigData vibrationDestination; - protected ConfigData trailColor; - protected ConfigData trailDuration; - protected ConfigData trailTargetOffset; - protected ConfigData trailOrigin; - protected ConfigData trailTarget; - protected ConfigData trailTargetRelativeOffset; + private ConfigData trailColor; + private ConfigData trailDuration; + private ConfigData trailTargetOffset; + private ConfigData trailOrigin; + private ConfigData trailTarget; + private ConfigData trailTargetRelativeOffset; + + private ConfigData geyserWaterBlocks; + private ConfigData geyserBurstImpulse; protected ConfigData count; - protected ConfigData radius; - protected ConfigData arrivalTime; - protected ConfigData shriekDelay; + private ConfigData radius; + private ConfigData arrivalTime; + private ConfigData shriekDelay; protected ConfigData speed; + protected ConfigData xSpread; protected ConfigData ySpread; protected ConfigData zSpread; - protected ConfigData dragonBreathPower; - protected ConfigData sculkChargeRotation; + + private ConfigData dragonBreathPower; + private ConfigData sculkChargeRotation; protected ConfigData force; - protected ConfigData staticDestination; + private ConfigData staticDestination; @Override public void loadFromConfig(ConfigurationSection config) { @@ -93,6 +99,9 @@ public void loadFromConfig(ConfigurationSection config) { trailTargetOffset = ConfigDataUtil.getVector(config, "trail.target-offset", new Vector()); trailTargetRelativeOffset = ConfigDataUtil.getVector(config, "trail.target-relative-offset", new Vector()); + geyserWaterBlocks = ConfigDataUtil.getInteger(config, "geyser.water-blocks"); + geyserBurstImpulse = ConfigDataUtil.getFloat(config, "geyser.burst-impulse"); + count = ConfigDataUtil.getInteger(config, "count", 5); radius = ConfigDataUtil.getInteger(config, "radius", 50); arrivalTime = ConfigDataUtil.getInteger(config, "arrival-time", -1); @@ -156,6 +165,13 @@ public Runnable playEffectLocation(Location location, SpellData data) { } protected Object getParticleData(@NotNull Particle particle, @Nullable Entity entity, @NotNull Location location, @NotNull SpellData data) { + Object nmsData = MagicSpells.getVolatileCodeHandler().getVolatileParticleData( + particle, + () -> geyserWaterBlocks.get(data), + () -> geyserBurstImpulse.get(data) + ); + if (nmsData != null) return nmsData; + Class type = particle.getDataType(); if (type == Color.class) { diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/ParticleCloudSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ParticleCloudSpell.java index 66546bde0..8936c2454 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/ParticleCloudSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ParticleCloudSpell.java @@ -20,6 +20,7 @@ import net.kyori.adventure.text.Component; import com.nisovin.magicspells.util.*; +import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.spells.TargetedSpell; import com.nisovin.magicspells.util.config.ConfigData; import com.nisovin.magicspells.spells.TargetedEntitySpell; @@ -41,18 +42,25 @@ public class ParticleCloudSpell extends TargetedSpell implements TargetedLocatio private final ConfigData spellOptions; private final ConfigData dustTransition; + private final ConfigData trailColor; + private final ConfigData trailDuration; + private final ConfigData trailTargetOffset; + private final ConfigData trailTargetRelativeOffset; + private final ConfigData particle; private final ConfigData waitTime; private final ConfigData shriekDelay; private final ConfigData ticksDuration; private final ConfigData durationOnUse; + private final ConfigData geyserWaterBlocks; private final ConfigData reapplicationDelay; private final ConfigData radius; private final ConfigData radiusOnUse; private final ConfigData radiusPerTick; private final ConfigData dragonBreathPower; + private final ConfigData geyserBurstImpulse; private final ConfigData sculkChargeRotation; private final ConfigData useGravity; @@ -82,6 +90,14 @@ public ParticleCloudSpell(MagicConfig config, String spellName) { ); spellOptions = ConfigDataUtil.getSpellOptions(config.getMainConfig(), internalKey + "spell.color", internalKey + "spell.power", null); + trailColor = getConfigDataColor("trail.color", null); + trailDuration = getConfigDataInt("trail.duration", _ -> null); + trailTargetOffset = getConfigDataVector("trail.target-offset", new Vector()); + trailTargetRelativeOffset = getConfigDataVector("trail.target-relative-offset", new Vector()); + + geyserWaterBlocks = getConfigDataInt("geyser.water-blocks", _ -> null); + geyserBurstImpulse = getConfigDataFloat("geyser.burst-impulse", _ -> null); + ConfigData material = getConfigDataMaterial("material", null); if (material.isConstant()) { Material mat = material.get(); @@ -180,7 +196,7 @@ private CastResult spawnCloud(SpellData data) { cloud.addCustomEffect(eff.get(finalData), true); Particle particle = this.particle.get(finalData); - cloud.setParticle(particle, getParticleData(particle, finalData)); + if (particle != Particle.VIBRATION) cloud.setParticle(particle, getParticleData(particle, finalData)); Component customName = this.customName.get(finalData); if (customName != null) { @@ -194,6 +210,13 @@ private CastResult spawnCloud(SpellData data) { } private Object getParticleData(@NotNull Particle particle, @NotNull SpellData data) { + Object nmsData = MagicSpells.getVolatileCodeHandler().getVolatileParticleData( + particle, + () -> geyserWaterBlocks.get(data), + () -> geyserBurstImpulse.get(data) + ); + if (nmsData != null) return nmsData; + Class type = particle.getDataType(); if (type == ItemStack.class) return item.get(data); @@ -208,6 +231,22 @@ private Object getParticleData(@NotNull Particle particle, @NotNull SpellData da rgbColor.get(data); } + if (type == Particle.Trail.class) { + Color color = trailColor.get(data); + if (color == null) return null; + + Integer duration = trailDuration.get(data); + if (duration == null) return null; + + Vector relativeOffset = trailTargetRelativeOffset.get(data); + Vector offset = trailTargetOffset.get(data); + + Location loc = data.location().add(offset); + if (!relativeOffset.isZero()) Util.applyRelativeOffset(loc, relativeOffset); + + return new Particle.Trail(loc, color, duration); + } + return switch (particle) { case SHRIEK -> shriekDelay.get(data); case DRAGON_BREATH -> dragonBreathPower.get(data); diff --git a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java index a222318c7..1cf9c2fb8 100644 --- a/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java +++ b/nms/latest/src/main/java/com/nisovin/magicspells/volatilecode/latest/VolatileCodeLatest.java @@ -4,6 +4,7 @@ import java.lang.invoke.VarHandle; import java.util.function.Consumer; import java.lang.invoke.MethodType; +import java.util.function.Supplier; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -11,6 +12,7 @@ import org.bukkit.World; import org.bukkit.Bukkit; +import org.bukkit.Particle; import org.bukkit.Location; import org.bukkit.util.Vector; import org.bukkit.entity.Entity; @@ -29,6 +31,7 @@ import org.bukkit.craftbukkit.entity.CraftLivingEntity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import net.kyori.adventure.text.Component; @@ -292,4 +295,30 @@ public long countEntitySchedulerTasks() { return count; } + @Override + public @Nullable Object getVolatileParticleData( + Particle particle, + Supplier geyserWaterBlocks, + Supplier geyserBurstImpulse + ) { + Class type = particle.getDataType(); + + if (type == Particle.Geyser.class) { + Integer waterBlocks = geyserWaterBlocks.get(); + if (waterBlocks == null) return null; + + return new Particle.Geyser(waterBlocks); + } + + if (type == Particle.GeyserBase.class) { + Integer waterBlocks = geyserWaterBlocks.get(); + Float burstImpulse = geyserBurstImpulse.get(); + if (waterBlocks == null || burstImpulse == null) return null; + + return new Particle.GeyserBase(waterBlocks, burstImpulse); + } + + return null; + } + } diff --git a/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java b/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java index c32ba68a4..d13bebd53 100644 --- a/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java +++ b/nms/shared/src/main/java/com/nisovin/magicspells/volatilecode/VolatileCodeHandle.java @@ -1,16 +1,22 @@ package com.nisovin.magicspells.volatilecode; +import java.util.function.Supplier; + import org.bukkit.World; +import org.bukkit.Particle; import org.bukkit.entity.*; import org.bukkit.Location; import org.bukkit.util.Vector; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; + import net.kyori.adventure.text.Component; +import com.nisovin.magicspells.util.glow.GlowManager; + import io.papermc.paper.advancement.AdvancementDisplay.Frame; -import com.nisovin.magicspells.util.glow.GlowManager; public abstract class VolatileCodeHandle { @@ -44,4 +50,13 @@ public VolatileCodeHandle(VolatileCodeHelper helper) { public abstract long countEntitySchedulerTasks(); + @Nullable + public Object getVolatileParticleData( + Particle particle, + Supplier geyserWaterBlocks, + Supplier geyserBurstImpulse + ) { + return null; + } + } From 1f9fc09dc0e84b3175bc8564683072cb8d1bdcd7 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 28 Jun 2026 17:55:07 +0200 Subject: [PATCH 13/13] build: Update EffectLib --- core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/build.gradle b/core/build.gradle index 0f291f3a2..8d8fe3b74 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -6,7 +6,7 @@ plugins { dependencies { shadow("org.apache.commons:commons-math4-core:4.0-beta1") shadow("com.github.ben-manes.caffeine:caffeine:3.2.2") - shadow("com.github.Chronoken:EffectLib:4e37625") + shadow("com.github.Chronoken:EffectLib:1da888c") shadow("org.incendo:cloud-paper:2.0.0-beta.16") shadow("org.incendo:cloud-minecraft-extras:2.0.0-beta.16") shadow("org.incendo:cloud-processors-requirements:1.0.0-rc.1")