From bc46d124a72aa4097be46f9c8e5ade630e1a1a83 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sat, 2 Aug 2025 10:49:08 +0200 Subject: [PATCH 01/14] fix: Duplicate transformer entries --- core/src/main/java/com/nisovin/magicspells/util/EntityData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index 9e67e4452..0d3dfbc3c 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -474,7 +474,7 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { Class entityClass = entityType.getEntityClass(); if (entityClass == null) continue; - for (Class transformerType : transformers.keys()) + for (Class transformerType : transformers.keySet()) if (transformerType.isAssignableFrom(entityClass)) options.putAll(entityType, transformers.get(transformerType)); } From aea39896b9eba8f3de694e269f8b09cabb30815a Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 8 Jun 2025 23:02:59 +0200 Subject: [PATCH 02/14] feat: ArmorStand slot locking - "disable-slots" (bool) - to EntityData and "armorstand" spell effect where it's "true" by default - "disable-slots" (EquipmentSlot enum list) - "equipment-locks" (list of sections with EquipmentSlot "slot" and ArmorStand.LockType "lock") --- .../effecttypes/ArmorStandEffect.java | 51 ++++++++--------- .../nisovin/magicspells/util/EntityData.java | 55 ++++++++++++++++--- .../util/config/ConfigDataUtil.java | 8 ++- 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java index e984dd9cf..d700dd117 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java @@ -2,7 +2,6 @@ import org.bukkit.Location; import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.configuration.ConfigurationSection; @@ -11,9 +10,11 @@ import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.EntityData; +import com.nisovin.magicspells.util.config.ConfigData; import com.nisovin.magicspells.spelleffects.SpellEffect; import com.nisovin.magicspells.util.magicitems.MagicItem; import com.nisovin.magicspells.util.magicitems.MagicItems; +import com.nisovin.magicspells.util.config.ConfigDataUtil; @Name("armorstand") public class ArmorStandEffect extends SpellEffect { @@ -27,9 +28,11 @@ public class ArmorStandEffect extends SpellEffect { private String customName; private boolean customNameVisible; + private ConfigData disableSlots; + private ItemStack headItem; - private ItemStack mainhandItem; - private ItemStack offhandItem; + private ItemStack offHandItem; + private ItemStack mainHandItem; @Override protected void loadFromConfig(ConfigurationSection config) { @@ -37,41 +40,39 @@ protected void loadFromConfig(ConfigurationSection config) { if (section == null) return; entityData = new EntityData(section); - entityData.setEntityType(data -> EntityType.ARMOR_STAND); gravity = section.getBoolean("gravity", false); customName = section.getString("custom-name", ""); customNameVisible = section.getBoolean("custom-name-visible", false); - String strMagicItem = section.getString("head", ""); - MagicItem magicItem = MagicItems.getMagicItemFromString(strMagicItem); - if (magicItem != null) headItem = magicItem.getItemStack(); + disableSlots = ConfigDataUtil.getBoolean(section, "disable-slots", true); - strMagicItem = section.getString("mainhand", ""); - magicItem = MagicItems.getMagicItemFromString(strMagicItem); - if (magicItem != null) mainhandItem = magicItem.getItemStack(); + MagicItem item = MagicItems.getMagicItemFromString(section.getString("head")); + if (item != null) headItem = item.getItemStack(); - strMagicItem = section.getString("offhand", ""); - magicItem = MagicItems.getMagicItemFromString(strMagicItem); - if (magicItem != null) offhandItem = magicItem.getItemStack(); + item = MagicItems.getMagicItemFromString(section.getString("offhand")); + if (item != null) offHandItem = item.getItemStack(); + item = MagicItems.getMagicItemFromString(section.getString("mainhand")); + if (item != null) mainHandItem = item.getItemStack(); } @Override protected ArmorStand playArmorStandEffectLocation(Location location, SpellData data) { - return (ArmorStand) entityData.spawn(location, data, entity -> { - ArmorStand armorStand = (ArmorStand) entity; - - armorStand.addScoreboardTag(ENTITY_TAG); - armorStand.setGravity(gravity); - armorStand.setSilent(true); - armorStand.customName(Util.getMiniMessage(customName, data)); - armorStand.setCustomNameVisible(customNameVisible); - - armorStand.setItem(EquipmentSlot.HEAD, headItem); - armorStand.setItem(EquipmentSlot.HAND, mainhandItem); - armorStand.setItem(EquipmentSlot.OFF_HAND, offhandItem); + return entityData.spawn(location, data, ArmorStand.class, stand -> { + stand.setSilent(true); + stand.addScoreboardTag(ENTITY_TAG); + + stand.setGravity(gravity); + stand.setCustomNameVisible(customNameVisible); + stand.customName(Util.getMiniMessage(customName, data)); + + if (this.disableSlots.get(data)) stand.setDisabledSlots(EquipmentSlot.values()); + + stand.setItem(EquipmentSlot.HEAD, headItem); + stand.setItem(EquipmentSlot.HAND, mainHandItem); + stand.setItem(EquipmentSlot.OFF_HAND, offHandItem); }); } diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index 0d3dfbc3c..c0faa0dff 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -213,6 +213,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addBoolean(transformers, config, "visible", true, ArmorStand.class, ArmorStand::setVisible, forceOptional); addBoolean(transformers, config, "has-arms", true, ArmorStand.class, ArmorStand::setArms, forceOptional); addBoolean(transformers, config, "has-base-plate", true, ArmorStand.class, ArmorStand::setBasePlate, forceOptional); + addBoolean(transformers, config, "disable-slots", false, ArmorStand.class, (stand, disabled) -> { + if (disabled) stand.setDisabledSlots(EquipmentSlot.values()); + }, forceOptional); addEulerAngle(transformers, config, "head-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setHeadPose, forceOptional); addEulerAngle(transformers, config, "body-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setBodyPose, forceOptional); @@ -221,6 +224,33 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addEulerAngle(transformers, config, "left-leg-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setLeftLegPose, forceOptional); addEulerAngle(transformers, config, "right-leg-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setRightLegPose, forceOptional); + for (String slotName : config.getStringList("disable-slots")) { + ConfigData slotData = ConfigDataUtil.getEnum(slotName, EquipmentSlot.class, null); + + transformers.put(ArmorStand.class, (ArmorStand stand, SpellData data) -> { + EquipmentSlot slot = slotData.get(data); + if (slot == null) return; + + stand.addDisabledSlots(slot); + }); + } + + for (Object object : config.getList("equipment-locks", new ArrayList<>())) { + if (!(object instanceof Map map)) continue; + ConfigurationSection section = ConfigReaderUtil.mapToSection(map); + + ConfigData slotData = ConfigDataUtil.getEnum(section, "slot", EquipmentSlot.class, null); + ConfigData lockData = ConfigDataUtil.getEnum(section, "lock", ArmorStand.LockType.class, null); + + transformers.put(ArmorStand.class, (ArmorStand stand, SpellData data) -> { + EquipmentSlot slot = slotData.get(data); + ArmorStand.LockType lock = lockData.get(data); + if (slot == null || lock == null) return; + + stand.addEquipmentLock(slot, lock); + }); + } + // Axolotl fallback( key -> addOptEnum(transformers, config, key, Axolotl.class, Axolotl.Variant.class, Axolotl::setVariant), @@ -511,6 +541,20 @@ public Entity spawn(@NotNull Location location, @Nullable Consumer consu @Nullable public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer consumer) { + EntityType type = this.entityType.get(data); + if (type == null || (!type.isSpawnable() && type != EntityType.FALLING_BLOCK && type != EntityType.ITEM)) + return null; + + Class entityClass = type.getEntityClass(); + if (entityClass == null) return null; + + return spawn(location, data, entityClass, entity -> { + if (consumer != null) consumer.accept(entity); + }); + } + + @NotNull + public T spawn(@NotNull Location location, @NotNull SpellData data, @NotNull Class entityClass, @Nullable Consumer consumer) { Location spawnLocation = location.clone(); Vector relativeOffset = this.relativeOffset.get(data); @@ -520,13 +564,6 @@ public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullab spawnLocation.setYaw(yaw.get(data).apply(spawnLocation.getYaw())); spawnLocation.setPitch(pitch.get(data).apply(spawnLocation.getPitch())); - EntityType type = this.entityType.get(data); - if (type == null || (!type.isSpawnable() && type != EntityType.FALLING_BLOCK && type != EntityType.ITEM)) - return null; - - Class entityClass = type.getEntityClass(); - if (entityClass == null) return null; - return spawnLocation.getWorld().spawn(spawnLocation, entityClass, entity -> { apply(entity, data); @@ -1003,6 +1040,10 @@ public ConfigData getRelativeOffset() { return relativeOffset; } + /** + * @deprecated Use {@link EntityData#spawn(Location, SpellData, Class, Consumer)} + */ + @Deprecated(forRemoval = true) public void setEntityType(ConfigData entityType) { this.entityType = entityType; } diff --git a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java index f976b3968..373d69139 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java +++ b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java @@ -509,7 +509,13 @@ public static > ConfigData getEnum(@NotNull ConfigurationSe @NotNull String path, @NotNull Class type, @Nullable T def) { - String value = config.getString(path); + return getEnum(config.getString(path), type, def); + } + + @NotNull + public static > ConfigData getEnum(@Nullable String value, + @NotNull Class type, + @Nullable T def) { if (value == null) return data -> def; try { From fb30fb4fcff08e560918bd9f5082b30c5628be75 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 5 Aug 2025 04:43:06 +0200 Subject: [PATCH 03/14] fix: Don't override EntityData Other changes: - Added "custom-name" & "custom-name-visibility". - "armorstand" effect options "gravity" and "custom-name-visibility" (now in EntityData) now have expression support. --- .../effecttypes/ArmorStandEffect.java | 23 ++++-------- .../effecttypes/EntityEffect.java | 11 +----- .../spells/targeted/SpawnEntitySpell.java | 7 ++-- .../nisovin/magicspells/util/EntityData.java | 36 +++++++++++++++---- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java index d700dd117..3d360b6f5 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java @@ -7,7 +7,6 @@ import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.Name; -import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.EntityData; import com.nisovin.magicspells.util.config.ConfigData; @@ -23,11 +22,7 @@ public class ArmorStandEffect extends SpellEffect { private EntityData entityData; - private boolean gravity; - - private String customName; - private boolean customNameVisible; - + private ConfigData gravity; private ConfigData disableSlots; private ItemStack headItem; @@ -41,11 +36,7 @@ protected void loadFromConfig(ConfigurationSection config) { entityData = new EntityData(section); - gravity = section.getBoolean("gravity", false); - - customName = section.getString("custom-name", ""); - customNameVisible = section.getBoolean("custom-name-visible", false); - + gravity = ConfigDataUtil.getBoolean(section, "gravity", false); disableSlots = ConfigDataUtil.getBoolean(section, "disable-slots", true); MagicItem item = MagicItems.getMagicItemFromString(section.getString("head")); @@ -60,20 +51,20 @@ protected void loadFromConfig(ConfigurationSection config) { @Override protected ArmorStand playArmorStandEffectLocation(Location location, SpellData data) { + boolean gravity = this.gravity.get(data); + boolean disableSlots = this.disableSlots.get(data); + return entityData.spawn(location, data, ArmorStand.class, stand -> { stand.setSilent(true); stand.addScoreboardTag(ENTITY_TAG); stand.setGravity(gravity); - stand.setCustomNameVisible(customNameVisible); - stand.customName(Util.getMiniMessage(customName, data)); - - if (this.disableSlots.get(data)) stand.setDisabledSlots(EquipmentSlot.values()); + if (disableSlots) stand.setDisabledSlots(EquipmentSlot.values()); stand.setItem(EquipmentSlot.HEAD, headItem); stand.setItem(EquipmentSlot.HAND, mainHandItem); stand.setItem(EquipmentSlot.OFF_HAND, offHandItem); - }); + }, stand -> stand.setPersistent(false)); } } diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java index cd5dcb816..340cdc62f 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java @@ -5,7 +5,6 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.Name; @@ -27,9 +26,7 @@ public class EntityEffect extends SpellEffect { private ConfigData duration; - private ConfigData silent; private ConfigData gravity; - private ConfigData enableAI; @Override protected void loadFromConfig(ConfigurationSection config) { @@ -40,9 +37,7 @@ protected void loadFromConfig(ConfigurationSection config) { duration = ConfigDataUtil.getInteger(section, "duration", 0); - silent = ConfigDataUtil.getBoolean(section, "silent", false); gravity = ConfigDataUtil.getBoolean(section, "gravity", false); - enableAI = ConfigDataUtil.getBoolean(section, "ai", true); } @Override @@ -50,11 +45,7 @@ protected Entity playEntityEffectLocation(Location location, SpellData data) { return entityData.spawn(location, data, entity -> { entity.addScoreboardTag(ENTITY_TAG); entity.setGravity(gravity.get(data)); - entity.setSilent(silent.get(data)); - entity.setPersistent(false); - - if (entity instanceof LivingEntity livingEntity) livingEntity.setAI(enableAI.get(data)); - }); + }, entity -> entity.setPersistent(false)); } @Override diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java index fb8587562..226501a9b 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java @@ -439,7 +439,10 @@ private CastResult spawnMob(Location source, SpellData data) { } SpellData finalData = data; - Entity entity = entityData.spawn(loc, data, mob -> prepMob(mob, finalData)); + Entity entity = entityData.spawn(loc, data, + mob -> prepMob(mob, finalData), + mob -> mob.setPersistent(!removeMob) + ); if (entity == null) return noTarget(data); UUID uuid = entity.getUniqueId(); @@ -483,8 +486,6 @@ private CastResult spawnMob(Location source, SpellData data) { } private void prepMob(Entity entity, SpellData data) { - if (removeMob) entity.setPersistent(false); - entity.setGravity(gravity.get(data)); entity.setInvulnerable(invulnerable.get(data)); diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index c0faa0dff..924bf3491 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -130,6 +130,8 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptBoolean(transformers, config, "glowing", Entity.class, Entity::setGlowing); addOptBoolean(transformers, config, "gravity", Entity.class, Entity::setGravity); addOptBoolean(transformers, config, "visible-by-default", Entity.class, Entity::setVisibleByDefault); + addOptBoolean(transformers, config, "custom-name-visible", Entity.class, Entity::setCustomNameVisible); + addOptVector(transformers, config, "velocity", Entity.class, Entity::setVelocity); if (config.isList("scoreboard-tags")) { @@ -170,6 +172,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Damageable addOptDouble(transformers, config, "health", Damageable.class, Damageable::setHealth); + // Nameable + addOptComponent(transformers, config, "custom-name", Nameable.class, Nameable::customName); + // LivingEntity addOptBoolean(transformers, config, "ai", LivingEntity.class, LivingEntity::setAI); addOptEquipment(transformers, config, "equipment.main-hand", EquipmentSlot.HAND); @@ -535,12 +540,22 @@ public Entity spawn(@NotNull Location location) { } @Nullable - public Entity spawn(@NotNull Location location, @Nullable Consumer consumer) { - return spawn(location, SpellData.NULL, consumer); + public Entity spawn(@NotNull Location location, @Nullable Consumer postConsumer) { + return spawn(location, SpellData.NULL, postConsumer); + } + + @Nullable + public Entity spawn(@NotNull Location location, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { + return spawn(location, SpellData.NULL, preConsumer, postConsumer); + } + + @Nullable + public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer postConsumer) { + return spawn(location, data, (Consumer) null, postConsumer); } @Nullable - public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer consumer) { + public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { EntityType type = this.entityType.get(data); if (type == null || (!type.isSpawnable() && type != EntityType.FALLING_BLOCK && type != EntityType.ITEM)) return null; @@ -549,12 +564,19 @@ public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullab if (entityClass == null) return null; return spawn(location, data, entityClass, entity -> { - if (consumer != null) consumer.accept(entity); + if (preConsumer != null) preConsumer.accept(entity); + }, entity -> { + if (postConsumer != null) postConsumer.accept(entity); }); } @NotNull - public T spawn(@NotNull Location location, @NotNull SpellData data, @NotNull Class entityClass, @Nullable Consumer consumer) { + public T spawn(@NotNull Location location, @NotNull SpellData data, @NotNull Class entityClass, @Nullable Consumer postConsumer) { + return spawn(location, data, entityClass, null, postConsumer); + } + + @NotNull + public T spawn(@NotNull Location location, @NotNull SpellData data, @NotNull Class entityClass, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { Location spawnLocation = location.clone(); Vector relativeOffset = this.relativeOffset.get(data); @@ -565,9 +587,11 @@ public T spawn(@NotNull Location location, @NotNull SpellData spawnLocation.setPitch(pitch.get(data).apply(spawnLocation.getPitch())); return spawnLocation.getWorld().spawn(spawnLocation, entityClass, entity -> { + if (preConsumer != null) preConsumer.accept(entity); + apply(entity, data); - if (consumer != null) consumer.accept(entity); + if (postConsumer != null) postConsumer.accept(entity); }); } From 9e45a657e927cace74855b02f38016d8e11b56a0 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 10 Jun 2025 04:34:43 +0200 Subject: [PATCH 04/14] fix: Don't choose by default --- .../magicspells/spells/MultiSpell.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java index 691bc8ab4..e3c71dcae 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java @@ -89,17 +89,18 @@ else if (action.isSpell()) { } } else { if (customSpellCastChance.get(data)) { - double total = 0; - for (ActionChance actionChance : actions) total += actionChance.chance; + double total = actions.stream().mapToDouble(ActionChance::chance).sum(); + if (total <= 0) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + + double selected = random.nextDouble(total); - double index = random.nextDouble(total); Action action = null; - double subChance = 0; + double current = 0; for (ActionChance actionChance : actions) { - subChance += actionChance.chance; - + current += actionChance.chance; + if (selected >= current) continue; action = actionChance.action; - if (subChance > index) break; + break; } if (action != null && action.isSpell()) action.getSpell().subcast(data); @@ -141,17 +142,19 @@ else if (action.isSpell()) { } } else { if (customSpellCastChance.get(subData)) { - double total = 0; - for (ActionChance actionChance : actions) total += actionChance.chance; + double total = actions.stream().mapToDouble(ActionChance::chance).sum(); + if (total <= 0) return false; + + double selected = random.nextDouble(total); - double index = random.nextDouble(total); Action action = null; - double subChance = 0; + double current = 0; for (ActionChance actionChance : actions) { - subChance += actionChance.chance; + current += actionChance.chance; + if (selected >= current) continue; action = actionChance.action; - if (subChance > index) break; + break; } if (action != null && action.isSpell()) action.getSpell().getSpell().castFromConsole(sender, args); From ac3a3b51afd321b86602eaef2c801884bf67fe78 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Thu, 7 Aug 2025 03:54:03 +0200 Subject: [PATCH 05/14] fix: Cumulative total --- .../nisovin/magicspells/spells/buff/MinionSpell.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java index 55aabf8f9..512c326e5 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java @@ -235,15 +235,14 @@ public boolean castBuff(SpellData data) { if (!(data.target() instanceof Player target)) return false; // Selecting the mob EntityType creatureType = null; - int num = random.nextInt(100); + int total = Arrays.stream(chances).sum(); + int num = random.nextInt(total); int n = 0; for (int i = 0; i < creatureTypes.length; i++) { - if (num < chances[i] + n) { - creatureType = creatureTypes[i]; - break; - } - n += chances[i]; + if (num >= n) continue; + creatureType = creatureTypes[i]; + break; } if (creatureType == null) return false; From 1891b619666017ed7e8c9edebbc4918d6166544b Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sat, 9 Aug 2025 15:00:12 +0200 Subject: [PATCH 06/14] fix: Default speed --- .../java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java b/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java index c54060e75..ceb2f7abc 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java +++ b/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java @@ -35,7 +35,7 @@ public PathToGoal(Mob mob, SpellData data) { @Override public boolean initialize(@Nullable ConfigurationSection config) { if (config == null) return false; - speed = ConfigDataUtil.getDouble(config, "speed", 0.2); + speed = ConfigDataUtil.getDouble(config, "speed", 1); position = ConfigDataUtil.getVector(config, "position", new Vector()); distanceAllowed = ConfigDataUtil.getDouble(config, "distance-allowed", 1); return true; From 24b48e3427e102754541c50663ffa57bc564be67 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 11 Aug 2025 16:53:08 +0200 Subject: [PATCH 07/14] refactor: Cleanup --- .../effecttypes/EntityEffect.java | 2 +- .../magicspells/spells/buff/MinionSpell.java | 173 +++++++++--------- .../spells/targeted/SpawnEntitySpell.java | 9 +- .../nisovin/magicspells/util/EntityData.java | 40 ++-- 4 files changed, 103 insertions(+), 121 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java index 340cdc62f..6d3fbe1e3 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java @@ -66,7 +66,7 @@ public Runnable playEffectLocation(Location location, SpellData data) { @Override public void turnOff() { - for (Entity entity : entities) entity.remove(); + entities.forEach(Entity::remove); entities.clear(); } diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java index 512c326e5..8662e8fe3 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java @@ -38,26 +38,24 @@ public class MinionSpell extends BuffSpell { - private final Map minions; - private final Map players; - private final Map targets; - - private final int[] chances; + private final Map minions = new HashMap<>(); + private final Map players = new HashMap<>(); + private final Map mobChances = new HashMap<>(); + private final Map targets = new ConcurrentHashMap<>(); private ValidTargetList minionTargetList; - private EntityType[] creatureTypes; - private final ConfigData powerAffectsHealth; - private final ConfigData gravity; - private final ConfigData baby; private boolean preventCombust; + private final ConfigData baby; + private final ConfigData gravity; + private final ConfigData powerAffectsHealth; - private final ConfigData powerHealthFactor; - private final ConfigData maxHealth; - private final ConfigData health; private double followRange; private double followSpeed; private double maxDistance; + private final ConfigData health; + private final ConfigData maxHealth; + private final ConfigData powerHealthFactor; private final ConfigData spawnOffset; @@ -92,29 +90,24 @@ public class MinionSpell extends BuffSpell { public MinionSpell(MagicConfig config, String spellName) { super(config, spellName); - minions = new HashMap<>(); - players = new HashMap<>(); - targets = new ConcurrentHashMap<>(); - // Formatted as - List c = getConfigStringList("mob-chances", new ArrayList<>()); - if (c.isEmpty()) c.add("Zombie 100"); - - creatureTypes = new EntityType[c.size()]; - chances = new int[c.size()]; - for (int i = 0; i < c.size(); i++) { - String[] data = c.get(i).split(" "); - EntityType creatureType = MobUtil.getEntityType(data[0]); - int chance = 0; - if (creatureType != null) { - try { - chance = Integer.parseInt(data[1]); - } catch (NumberFormatException e) { - // No op - } + List mobChanceList = getConfigStringList("mob-chances", new ArrayList<>()); + if (mobChanceList.isEmpty()) mobChanceList.add("Zombie 100"); + + for (int i = 0; i < mobChanceList.size(); i++) { + String[] splits = mobChanceList.get(i).split(" "); + + EntityType type = MobUtil.getEntityType(splits[0]); + if (type == null) { + MagicSpells.error("MinionSpell '" + internalName + "' has an invalid Entity Type specified in 'mob-chances' at index " + i + ": " + splits[0]); + continue; + } + + try { + mobChances.put(type, Integer.parseInt(splits[1])); + } catch (NumberFormatException ignored) { + MagicSpells.error("MinionSpell '" + internalName + "' has an invalid chance value specified in 'mob-chances' at index " + i + ": " + splits[1]); } - creatureTypes[i] = creatureType; - chances[i] = chance; } // Potion effects @@ -196,22 +189,24 @@ public MinionSpell(MagicConfig config, String spellName) { leggingsDropChance = getConfigFloat("leggings-drop-chance", 0) / 100F; bootsDropChance = getConfigFloat("boots-drop-chance", 0) / 100F; + minionName = getConfigString("minion-name", ""); spawnSpellName = getConfigString("spell-on-spawn", ""); - attackSpellName = getConfigString("spell-on-attack", ""); deathSpellName = getConfigString("spell-on-death", ""); + attackSpellName = getConfigString("spell-on-attack", ""); spawnOffset = getConfigDataVector("spawn-offset", new Vector(1, 0, 0)); - followRange = getConfigDouble("follow-range", 1.5) * -1; + + health = getConfigDataDouble("health", 20); followSpeed = getConfigDouble("follow-speed", 1); maxDistance = getConfigDouble("max-distance", 30); - powerAffectsHealth = getConfigDataBoolean("power-affects-health", false); - powerHealthFactor = getConfigDataDouble("power-health-factor", 1); maxHealth = getConfigDataDouble("max-health", 20); - health = getConfigDataDouble("health", 20); - minionName = getConfigString("minion-name", ""); - gravity = getConfigDataBoolean("gravity", true); + followRange = getConfigDouble("follow-range", 1.5) * -1; + powerHealthFactor = getConfigDataDouble("power-health-factor", 1); + baby = getConfigDataBoolean("baby", false); + gravity = getConfigDataBoolean("gravity", true); preventCombust = getConfigBoolean("prevent-sun-burn", true); + powerAffectsHealth = getConfigDataBoolean("power-affects-health", false); } @Override @@ -234,43 +229,65 @@ public void initialize() { public boolean castBuff(SpellData data) { if (!(data.target() instanceof Player target)) return false; // Selecting the mob - EntityType creatureType = null; - int total = Arrays.stream(chances).sum(); - int num = random.nextInt(total); - int n = 0; - for (int i = 0; i < creatureTypes.length; i++) { - n += chances[i]; - if (num >= n) continue; - creatureType = creatureTypes[i]; + EntityType entityType = null; + int total = mobChances.values().stream().mapToInt(Integer::intValue).sum(); + int selected = random.nextInt(total); + + int current = 0; + for (Map.Entry entry : mobChances.entrySet()) { + current += entry.getValue(); + if (selected >= current) continue; + + entityType = entry.getKey(); break; } - if (creatureType == null) return false; + if (entityType == null) { + MagicSpells.error("MinionSpell '" + internalName + "' is missing entity type!"); + return false; + } + Class entityClass = entityType.getEntityClass(); + if (entityClass == null || !Mob.class.isAssignableFrom(entityClass)) { + MagicSpells.error("MinionSpell '" + internalName + "' can only summon mobs!"); + return false; + } + //noinspection unchecked + Class mobClass = (Class) entityClass; // Spawn location Location loc = target.getLocation().clone(); loc.setPitch(0); - Vector spawnOffset = this.spawnOffset.get(data); loc.add(0, spawnOffset.getY(), 0); Util.applyRelativeOffset(loc, spawnOffset.setY(0)); // Spawn creature - LivingEntity minion = (LivingEntity) target.getWorld().spawnEntity(loc, creatureType); - if (!(minion instanceof Mob)) { - minion.remove(); - MagicSpells.error("MinionSpell '" + internalName + "' can only summon mobs!"); - return false; - } + Mob minion = target.getWorld().spawn(loc, mobClass, mob -> { + prepareMob(mob, target, data); - if (minion instanceof Ageable ageable) { + if (!(mob instanceof Ageable ageable)) return; if (baby.get(data)) ageable.setBaby(); else ageable.setAdult(); + }); + + if (spawnSpell != null) { + SpellData castData = data.builder().caster(target).target(minion).location(minion.getLocation()).recipient(null).build(); + spawnSpell.subcast(castData); } + minions.put(target.getUniqueId(), minion); + players.put(minion, target.getUniqueId()); + return true; + } + + private void prepareMob(Mob minion, Player target, SpellData data) { minion.setGravity(gravity.get(data)); - minion.customName(Util.getMiniMessage(MagicSpells.doReplacements(minionName, target, data, "%c", target.getName()))); - minion.setCustomNameVisible(true); + + String customName = MagicSpells.doReplacements(minionName, target, data, "%c", target.getName()); + if (!customName.isEmpty()) { + minion.customName(Util.getMiniMessage(customName)); + minion.setCustomNameVisible(true); + } double powerHealthFactor = this.powerHealthFactor.get(data); double maxHealth = this.maxHealth.get(data); @@ -283,15 +300,8 @@ public boolean castBuff(SpellData data) { minion.setHealth(health); } - if (spawnSpell != null) { - SpellData castData = data.builder().caster(target).target(minion).location(minion.getLocation()).recipient(null).build(); - spawnSpell.subcast(castData); - } - - // Apply potion effects if (potionEffects != null) minion.addPotionEffects(potionEffects); - // Apply attributes if (attributes != null) { attributes.asMap().forEach((attribute, modifiers) -> { AttributeInstance attributeInstance = minion.getAttribute(attribute); @@ -301,8 +311,7 @@ public boolean castBuff(SpellData data) { }); } - // Equip the minion - final EntityEquipment eq = minion.getEquipment(); + EntityEquipment eq = minion.getEquipment(); if (mainHandItem != null) eq.setItemInMainHand(mainHandItem.clone()); if (offHandItem != null) eq.setItemInOffHand(offHandItem.clone()); if (helmet != null) eq.setHelmet(helmet.clone()); @@ -310,17 +319,12 @@ public boolean castBuff(SpellData data) { if (leggings != null) eq.setLeggings(leggings.clone()); if (boots != null) eq.setBoots(boots.clone()); - // Equipment drop chance eq.setItemInMainHandDropChance(mainHandItemDropChance); eq.setItemInOffHandDropChance(offHandItemDropChance); eq.setHelmetDropChance(helmetDropChance); eq.setChestplateDropChance(chestplateDropChance); eq.setLeggingsDropChance(leggingsDropChance); eq.setBootsDropChance(bootsDropChance); - - minions.put(target.getUniqueId(), minion); - players.put(minion, target.getUniqueId()); - return true; } @Override @@ -329,7 +333,7 @@ public boolean isActive(LivingEntity entity) { } public boolean isMinion(LivingEntity entity) { - return minions.containsValue(entity); + return minions.values().stream().anyMatch(mob -> mob.getUniqueId().equals(entity.getUniqueId())); } @Override @@ -355,7 +359,7 @@ public void onEntityTarget(EntityTargetEvent e) { Player pl = Bukkit.getPlayer(players.get(minion)); if (pl == null) return; - if (targets.get(pl.getUniqueId()) == null || !targets.containsKey(pl.getUniqueId()) || !targets.get(pl.getUniqueId()).isValid()) { + if (!targets.containsKey(pl.getUniqueId()) || !targets.get(pl.getUniqueId()).isValid()) { e.setCancelled(true); return; } @@ -452,7 +456,7 @@ else if (e.getDamager() instanceof Projectile projectile) { // Check if the entity can be targeted by the minion if (!minionTargetList.canTarget(pl, entity)) return; - targets.put(pl.getUniqueId(),entity); + targets.put(pl.getUniqueId(), entity); MobUtil.setTarget(minions.get(pl.getUniqueId()), entity); addUseAndChargeCost(pl); @@ -478,7 +482,7 @@ else if (e.getDamager() instanceof Projectile projectile) { Location loc = pl.getLocation().clone(); loc.add(loc.getDirection().setY(0).normalize().multiply(followRange)); - ((Mob) minions.get(pl.getUniqueId())).getPathfinder().moveTo(loc, followSpeed); + minions.get(pl.getUniqueId()).getPathfinder().moveTo(loc, followSpeed); } } } @@ -526,10 +530,9 @@ public void onPlayerMove(PlayerMoveEvent e) { if (e.getFrom().getBlock().equals(e.getTo().getBlock())) return; Player pl = e.getPlayer(); if (!isActive(pl)) return; - LivingEntity minion = minions.get(pl.getUniqueId()); + Mob minion = minions.get(pl.getUniqueId()); if ((pl.getWorld().equals(minion.getWorld()) && pl.getLocation().distanceSquared(minion.getLocation()) > maxDistance * maxDistance) || targets.get(pl.getUniqueId()) == null || !targets.containsKey(pl.getUniqueId())) { - // The minion has a target, but he is far away from his owner, remove his current target if (targets.get(pl.getUniqueId()) != null) { targets.remove(pl.getUniqueId()); @@ -539,7 +542,7 @@ public void onPlayerMove(PlayerMoveEvent e) { // The distance between minion and his owner is greater that the defined max distance or the minion has no targets, he will follow his owner Location loc = pl.getLocation().clone(); loc.add(loc.getDirection().setY(0).normalize().multiply(followRange)); - ((Mob) minions.get(pl.getUniqueId())).getPathfinder().moveTo(loc, followSpeed); + minion.getPathfinder().moveTo(loc, followSpeed); } } @@ -566,7 +569,7 @@ public void onEntityUnload(ChunkUnloadEvent event) { } } - public Map getMinions() { + public Map getMinions() { return minions; } @@ -594,14 +597,6 @@ public void setMinionTargetList(ValidTargetList minionTargetList) { this.minionTargetList = minionTargetList; } - public EntityType[] getCreatureTypes() { - return creatureTypes; - } - - public void setCreatureTypes(EntityType[] creatureTypes) { - this.creatureTypes = creatureTypes; - } - public boolean shouldPreventCombust() { return preventCombust; } diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java index 226501a9b..947784bab 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java @@ -33,7 +33,6 @@ import net.kyori.adventure.text.Component; -import com.destroystokyo.paper.entity.ai.MobGoals; import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent; import com.nisovin.magicspells.util.*; @@ -185,11 +184,11 @@ public SpawnEntitySpell(MagicConfig config, String spellName) { setOwner = getConfigDataBoolean("set-owner", true); removeAI = getConfigDataBoolean("remove-ai", false); invulnerable = getConfigDataBoolean("invulnerable", false); + cancelAttack = getConfigDataBoolean("cancel-attack", true); useCasterName = getConfigDataBoolean("use-caster-name", false); centerLocation = getConfigDataBoolean("center-location", false); addLookAtPlayerAI = getConfigDataBoolean("add-look-at-player-ai", false); allowSpawnInMidair = getConfigDataBoolean("allow-spawn-in-midair", false); - cancelAttack = getConfigDataBoolean("cancel-attack", true); attackSpellName = getConfigString("attack-spell", ""); spellOnSpawnName = getConfigString("spell-on-spawn", ""); @@ -546,10 +545,8 @@ private void prepMob(Entity entity, SpellData data) { if (removeAI.get(data)) { if (addLookAtPlayerAI.get(data) && livingEntity instanceof Mob mob) { - MobGoals mobGoals = Bukkit.getMobGoals(); - - mobGoals.removeAllGoals(mob); - mobGoals.addGoal(mob, 1, new LookAtEntityTypeGoal(mob, data)); + Bukkit.getMobGoals().removeAllGoals(mob); + Bukkit.getMobGoals().addGoal(mob, 1, new LookAtEntityTypeGoal(mob, data)); } else livingEntity.setAI(false); } diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index 924bf3491..d56f06424 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -134,16 +134,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptVector(transformers, config, "velocity", Entity.class, Entity::setVelocity); - if (config.isList("scoreboard-tags")) { - List tagStrings = config.getStringList("scoreboard-tags"); - if (!tagStrings.isEmpty()) { - List> tags = new ArrayList<>(); - for (String tagString : tagStrings) tags.add(ConfigDataUtil.getString(tagString)); - - transformers.put(Entity.class, (Entity entity, SpellData data) -> { - for (ConfigData tag : tags) entity.addScoreboardTag(tag.get(data)); - }); - } + for (String tagString : config.getStringList("scoreboard-tags")) { + ConfigData tag = ConfigDataUtil.getString(tagString); + transformers.put(Entity.class, (Entity entity, SpellData data) -> entity.addScoreboardTag(tag.get(data))); } // Ageable @@ -155,10 +148,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptInteger(transformers, config, "age", Ageable.class, Ageable::setAge); // Attributable - List attributeModifierStrings = config.getList("attribute-modifiers"); - if (attributeModifierStrings != null && !attributeModifierStrings.isEmpty()) { - Multimap attributeModifiers = AttributeHandler.getAttributeModifiers(attributeModifierStrings, null); - + List attributeModifierStrings = config.getList("attribute-modifiers", new ArrayList<>()); + Multimap attributeModifiers = AttributeHandler.getAttributeModifiers(attributeModifierStrings, null); + if (!attributeModifiers.isEmpty()) { transformers.put(Attributable.class, (Attributable entity, SpellData data) -> { attributeModifiers.asMap().forEach((attribute, modifiers) -> { AttributeInstance attributeInstance = entity.getAttribute(attribute); @@ -185,14 +177,11 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptEquipment(transformers, config, "equipment.boots", EquipmentSlot.FEET); addOptEquipment(transformers, config, "equipment.body", Mob.class, EquipmentSlot.BODY); - List potionEffectData = config.getList("potion-effects"); - if (potionEffectData != null && !potionEffectData.isEmpty()) { - List> effects = Util.getPotionEffects(potionEffectData, null); - if (effects != null) { - transformers.put(LivingEntity.class, (LivingEntity entity, SpellData data) -> { - effects.forEach(effect -> entity.addPotionEffect(effect.get(data))); - }); - } + List> potionEffects = Util.getPotionEffects(config.getList("potion-effects", new ArrayList<>()), null); + if (potionEffects != null) { + transformers.put(LivingEntity.class, (LivingEntity entity, SpellData data) -> { + potionEffects.forEach(effect -> entity.addPotionEffect(effect.get(data))); + }); } // Mob @@ -316,8 +305,10 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Item dropItemMaterial = addOptMaterial(transformers, config, "material", Item.class, (item, material) -> item.setItemStack(new ItemStack(material))); - addOptBoolean(transformers, config, "will-age", Item.class, Item::setWillAge); + addOptInteger(transformers, config, "pickup-delay", Item.class, Item::setPickupDelay); + + addOptBoolean(transformers, config, "will-age", Item.class, Item::setWillAge); addOptBoolean(transformers, config, "can-mob-pickup", Item.class, Item::setCanMobPickup); addOptBoolean(transformers, config, "can-player-pickup", Item.class, Item::setCanPlayerPickup); @@ -557,8 +548,7 @@ public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullab @Nullable public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { EntityType type = this.entityType.get(data); - if (type == null || (!type.isSpawnable() && type != EntityType.FALLING_BLOCK && type != EntityType.ITEM)) - return null; + if (type == null || !type.isSpawnable()) return null; Class entityClass = type.getEntityClass(); if (entityClass == null) return null; From d8db02bc016a7af8c5f8a0fb18be2233ba4bc8a8 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Thu, 7 Aug 2025 02:12:55 +0200 Subject: [PATCH 08/14] feat: Add ItemStack support to Item --- .../spells/buff/ext/DisguiseSpell.java | 3 +-- .../nisovin/magicspells/util/EntityData.java | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java index 736a58188..bb70a70a5 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java @@ -7,7 +7,6 @@ import org.bukkit.DyeColor; import org.bukkit.entity.Villager; import org.bukkit.entity.LivingEntity; -import org.bukkit.inventory.ItemStack; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.util.DependsOn; @@ -151,7 +150,7 @@ public boolean castBuff(SpellData data) { creeperWatcher.setPowered(entityData.getPowered().get(data)); if (watcher instanceof DroppedItemWatcher droppedItemWatcher) - droppedItemWatcher.setItemStack(new ItemStack(entityData.getDroppedItemStack().get(data))); + droppedItemWatcher.setItemStack(entityData.getDroppedItemStack().get(data)); if (watcher instanceof EndermanWatcher endermanWatcher) endermanWatcher.setItemInMainHand(entityData.getCarriedBlockData().get(data).getMaterial()); diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index d56f06424..5325fa071 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -85,7 +85,7 @@ public class EntityData { private final ConfigData horseStyle; // Item - private final ConfigData dropItemMaterial; + private final ConfigData dropItem; // Llama private final ConfigData llamaColor; @@ -304,7 +304,10 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { ); // Item - dropItemMaterial = addOptMaterial(transformers, config, "material", Item.class, (item, material) -> item.setItemStack(new ItemStack(material))); + dropItem = fallback( + key -> addOptItemStack(transformers, config, key, Item.class, Item::setItemStack), + "material", "item" + ); addOptInteger(transformers, config, "pickup-delay", Item.class, Item::setPickupDelay); @@ -748,17 +751,20 @@ private ConfigData addOptBlockData(Multimap, Transformer return supplier; } - private void addOptItemStack(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { + private ConfigData addOptItemStack(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { ConfigData supplier = ConfigDataUtil.getString(config, name, null); if (supplier.isConstant()) { ItemStack item = getItemStack(supplier.get()); - if (item == null) return; + if (item == null) return data -> null; transformers.put(type, new TransformerImpl<>(data -> item, setter, true)); - return; + return data -> item; } - transformers.put(type, new TransformerImpl<>(data -> getItemStack(supplier.get(data)), setter, true)); + ConfigData itemSupplier = data -> getItemStack(supplier.get(data)); + transformers.put(type, new TransformerImpl<>(itemSupplier, setter, true)); + + return itemSupplier; } private ItemStack getItemStack(String string) { @@ -1063,8 +1069,8 @@ public void setEntityType(ConfigData entityType) { } @ApiStatus.Internal - public ConfigData getDroppedItemStack() { - return dropItemMaterial; + public ConfigData getDroppedItemStack() { + return dropItem; } @ApiStatus.Internal From 60bb05eb3f1ce43529ee9fe61323a44c5ca6d173 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 5 Aug 2025 05:16:21 +0200 Subject: [PATCH 09/14] feat: Add EntityData support --- .../magicspells/spells/buff/MinionSpell.java | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java index 8662e8fe3..84d634494 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java @@ -22,6 +22,7 @@ import org.bukkit.attribute.AttributeModifier; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.configuration.ConfigurationSection; import io.papermc.paper.entity.TeleportFlag; @@ -38,11 +39,19 @@ public class MinionSpell extends BuffSpell { + private static final DeprecationNotice BABY_DEPRECATION_NOTICE = new DeprecationNotice( + "The 'baby' option of '.buff.MinionSpell' overrides the one from 'minion' Entity Data option.", + "Use the 'baby' option in the 'minion' Entity Data to avoid the override.", + "https://github.com/TheComputerGeek2/MagicSpells/wiki/Deprecations#buffminionspell-baby" + ); + private final Map minions = new HashMap<>(); private final Map players = new HashMap<>(); private final Map mobChances = new HashMap<>(); private final Map targets = new ConcurrentHashMap<>(); + private EntityData entityData; + private ValidTargetList minionTargetList; private boolean preventCombust; @@ -90,6 +99,9 @@ public class MinionSpell extends BuffSpell { public MinionSpell(MagicConfig config, String spellName) { super(config, spellName); + ConfigurationSection minionSection = getConfigSection("minion"); + if (minionSection != null) entityData = new EntityData(minionSection); + // Formatted as List mobChanceList = getConfigStringList("mob-chances", new ArrayList<>()); if (mobChanceList.isEmpty()) mobChanceList.add("Zombie 100"); @@ -207,6 +219,10 @@ public MinionSpell(MagicConfig config, String spellName) { gravity = getConfigDataBoolean("gravity", true); preventCombust = getConfigBoolean("prevent-sun-burn", true); powerAffectsHealth = getConfigDataBoolean("power-affects-health", false); + + MagicSpells.getDeprecationManager().addDeprecation(this, BABY_DEPRECATION_NOTICE, + entityData != null && (!baby.isConstant() || baby.get()) + ); } @Override @@ -230,17 +246,20 @@ public boolean castBuff(SpellData data) { if (!(data.target() instanceof Player target)) return false; // Selecting the mob EntityType entityType = null; - int total = mobChances.values().stream().mapToInt(Integer::intValue).sum(); - int selected = random.nextInt(total); + if (entityData == null) { + int total = mobChances.values().stream().mapToInt(Integer::intValue).sum(); + int selected = random.nextInt(total); - int current = 0; - for (Map.Entry entry : mobChances.entrySet()) { - current += entry.getValue(); - if (selected >= current) continue; + int current = 0; + for (Map.Entry entry : mobChances.entrySet()) { + current += entry.getValue(); + if (selected >= current) continue; - entityType = entry.getKey(); - break; + entityType = entry.getKey(); + break; + } } + else entityType = entityData.getEntityType().get(data); if (entityType == null) { MagicSpells.error("MinionSpell '" + internalName + "' is missing entity type!"); @@ -262,13 +281,15 @@ public boolean castBuff(SpellData data) { Util.applyRelativeOffset(loc, spawnOffset.setY(0)); // Spawn creature - Mob minion = target.getWorld().spawn(loc, mobClass, mob -> { + Mob minion; + if (entityData == null) minion = target.getWorld().spawn(loc, mobClass, mob -> { prepareMob(mob, target, data); if (!(mob instanceof Ageable ageable)) return; if (baby.get(data)) ageable.setBaby(); else ageable.setAdult(); }); + else minion = entityData.spawn(loc, data, mobClass, mob -> prepareMob(mob, target, data), null); if (spawnSpell != null) { SpellData castData = data.builder().caster(target).target(minion).location(minion.getLocation()).recipient(null).build(); From 341d80f3371a44a9acf5275313469eb35a4caca7 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Fri, 8 Aug 2025 13:07:37 +0200 Subject: [PATCH 10/14] feat: Add more EntityData options --- .../nisovin/magicspells/util/EntityData.java | 162 +++++++++++++++++- .../util/config/ConfigDataUtil.java | 3 + 2 files changed, 161 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index 5325fa071..be34fa9ff 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -4,10 +4,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.ApiStatus; -import java.util.Map; -import java.util.List; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.BiConsumer; @@ -18,8 +15,13 @@ import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; +import net.kyori.adventure.util.TriState; import net.kyori.adventure.text.Component; +import com.destroystokyo.paper.entity.ai.Goal; +import com.destroystokyo.paper.entity.ai.GoalKey; +import com.destroystokyo.paper.entity.ai.GoalType; + import org.bukkit.*; import org.bukkit.entity.*; import org.bukkit.util.Vector; @@ -34,14 +36,17 @@ import org.bukkit.inventory.EntityEquipment; import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeModifier; +import org.bukkit.entity.minecart.CommandMinecart; import org.bukkit.configuration.ConfigurationSection; +import io.papermc.paper.entity.Frictional; import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.entity.CollarColorable; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.ai.CustomGoal; import com.nisovin.magicspells.util.config.ConfigData; import com.nisovin.magicspells.util.config.FunctionData; import com.nisovin.magicspells.util.magicitems.MagicItem; @@ -129,9 +134,16 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptBoolean(transformers, config, "silent", Entity.class, Entity::setSilent); addOptBoolean(transformers, config, "glowing", Entity.class, Entity::setGlowing); addOptBoolean(transformers, config, "gravity", Entity.class, Entity::setGravity); + addOptBoolean(transformers, config, "no-physics", Entity.class, Entity::setNoPhysics); + addOptBoolean(transformers, config, "persistent", Entity.class, Entity::setPersistent); + addOptBoolean(transformers, config, "visual-fire", Entity.class, Entity::setVisualFire); + addOptBoolean(transformers, config, "invulnerable", Entity.class, Entity::setInvulnerable); addOptBoolean(transformers, config, "visible-by-default", Entity.class, Entity::setVisibleByDefault); addOptBoolean(transformers, config, "custom-name-visible", Entity.class, Entity::setCustomNameVisible); + addOptInteger(transformers, config, "fire-ticks", Entity.class, Entity::setFireTicks); + addOptInteger(transformers, config, "freeze-ticks", Entity.class, Entity::setFreezeTicks); + addOptVector(transformers, config, "velocity", Entity.class, Entity::setVelocity); for (String tagString : config.getStringList("scoreboard-tags")) { @@ -169,6 +181,10 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // LivingEntity addOptBoolean(transformers, config, "ai", LivingEntity.class, LivingEntity::setAI); + addOptBoolean(transformers, config, "can-pickup-items", LivingEntity.class, LivingEntity::setCanPickupItems); + + addOptDouble(transformers, config, "max-health", LivingEntity.class, Util::setMaxHealth); + addOptEquipment(transformers, config, "equipment.main-hand", EquipmentSlot.HAND); addOptEquipment(transformers, config, "equipment.off-hand", EquipmentSlot.OFF_HAND); addOptEquipment(transformers, config, "equipment.helmet", EquipmentSlot.HEAD); @@ -185,6 +201,8 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { } // Mob + addOptBoolean(transformers, config, "aware", Mob.class, Mob::setAware); + addOptEquipmentDropChance(transformers, config, "equipment.main-hand-drop-chance", EquipmentSlot.HAND); addOptEquipmentDropChance(transformers, config, "equipment.off-hand-drop-chance", EquipmentSlot.OFF_HAND); addOptEquipmentDropChance(transformers, config, "equipment.helmet-drop-chance", EquipmentSlot.HEAD); @@ -195,6 +213,12 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Tameable tamed = addBoolean(transformers, config, "tamed", false, Tameable.class, Tameable::setTamed, forceOptional); + if (config.getBoolean("tamed-owner")) { + transformers.put(Tameable.class, (Tameable tameable, SpellData data) -> { + if (!(data.recipient() instanceof AnimalTamer tamer)) return; + tameable.setOwner(tamer); + }); + } // AbstractHorse saddled = addBoolean(transformers, config, "saddled", false, AbstractHorse.class, (horse, saddled) -> { @@ -263,6 +287,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "collar-color", "color" ); + // CommandMinecart + addOptString(transformers, config, "command", CommandMinecart.class, CommandMinecart::setCommand); + // ChestedHorse chested = addBoolean(transformers, config, "chested", false, ChestedHorse.class, ChestedHorse::setCarryingChest, forceOptional); @@ -281,18 +308,37 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "falling-block", "material" ); + addOptBoolean(transformers, config, "cancel-drop", FallingBlock.class, FallingBlock::setCancelDrop); + addOptBoolean(transformers, config, "hurt-entities", FallingBlock.class, FallingBlock::setHurtEntities); + + addOptFloat(transformers, config, "damage-per-block", FallingBlock.class, FallingBlock::setDamagePerBlock); + + addOptInteger(transformers, config, "max-damage", FallingBlock.class, FallingBlock::setMaxDamage); + // Fox fallback( key -> addOptEnum(transformers, config, key, Fox.class, Fox.Type.class, Fox::setFoxType), "fox-type", "type" ); + // Frictional + addOptEnum(transformers, config, "friction-state", Frictional.class, TriState.class, Frictional::setFrictionState); + // Frog fallback( key -> addOptRegistryEntry(transformers, config, key, Frog.class, Registry.FROG_VARIANT, Frog::setVariant), "frog-variant", "type" ); + // Goat + addOptBoolean(transformers, config, "left-horn", Goat.class, Goat::setLeftHorn); + addOptBoolean(transformers, config, "right-horn", Goat.class, Goat::setRightHorn); + addOptBoolean(transformers, config, "screaming", Goat.class, Goat::setScreaming); + + // Hoglin + addOptBoolean(transformers, config, "immune-to-zombification", Hoglin.class, Hoglin::setImmuneToZombification); + addOptBoolean(transformers, config, "able-to-be-hunted", Hoglin.class, Hoglin::setIsAbleToBeHunted); + // Horse horseColor = fallback( key -> addOptEnum(transformers, config, key, Horse.class, Horse.Color.class, Horse::setColor), @@ -330,6 +376,15 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "llama-decor", "material" ); + // Minecart + addOptDouble(transformers, config, "max-speed", Minecart.class, Minecart::setMaxSpeed); + + addOptBoolean(transformers, config, "slow-when-empty", Minecart.class, Minecart::setSlowWhenEmpty); + + addOptInteger(transformers, config, "display-block-offset", Minecart.class, Minecart::setDisplayBlockOffset); + + addOptBlockData(transformers, config, "display-block", Minecart.class, Minecart::setDisplayBlockData); + // Mushroom Cow fallback( key -> addOptEnum(transformers, config, key, MushroomCow.class, MushroomCow.Variant.class, MushroomCow::setVariant), @@ -350,6 +405,13 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addInteger(transformers, config, "size", 0, Phantom.class, Phantom::setSize, forceOptional); addOptBoolean(transformers, config, "should-burn-in-day", Phantom.class, Phantom::setShouldBurnInDay); + // Piglin + addOptBoolean(transformers, config, "able-to-hunt", Piglin.class, Piglin::setIsAbleToHunt); + addOptInteger(transformers, config, "dancing", Piglin.class, Piglin::setDancing); + + // Piglin Abstract + addOptBoolean(transformers, config, "immune-to-zombification", PiglinAbstract.class, PiglinAbstract::setImmuneToZombification); + // Puffer Fish size = addInteger(transformers, config, "size", 0, PufferFish.class, PufferFish::setPuffState, forceOptional); @@ -359,6 +421,11 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "rabbit-type", "type" ); + // Raider + addOptBoolean(transformers, config, "patrol-leader", Raider.class, Raider::setPatrolLeader); + addOptBoolean(transformers, config, "can-join-raid", Raider.class, Raider::setCanJoinRaid); + addOptBoolean(transformers, config, "celebrating", Raider.class, Raider::setCelebrating); + // Sheep sheared = addBoolean(transformers, config, "sheared", false, Sheep.class, Sheep::setSheared, forceOptional); color = fallback( @@ -395,6 +462,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "tropical-fish.pattern", "type" ); + // Salmon + addOptEnum(transformers, config, "salmon-variant", Salmon.class, Salmon.Variant.class, Salmon::setVariant); + // Villager profession = fallback( key -> addOptRegistryEntry(transformers, config, key, Villager.class, Registry.VILLAGER_PROFESSION, Villager::setProfession), @@ -402,6 +472,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { ); addOptRegistryEntry(transformers, config, "villager-type", Villager.class, Registry.VILLAGER_TYPE, Villager::setVillagerType); + // Vindicator + addOptBoolean(transformers, config, "johnny", Vindicator.class, Vindicator::setJohnny); + // Wolf addBoolean(transformers, config, "angry", false, Wolf.class, Wolf::setAngry, forceOptional); addOptRegistryEntry(transformers, config, "wolf-variant", Wolf.class, RegistryKey.WOLF_VARIANT, Wolf::setVariant); @@ -409,6 +482,10 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Zombie addOptBoolean(transformers, config, "should-burn-in-day", Zombie.class, Zombie::setShouldBurnInDay); + // Zombie Villager + addOptRegistryEntry(transformers, config, "villager-profession", ZombieVillager.class, Registry.VILLAGER_PROFESSION, ZombieVillager::setVillagerProfession); + addOptRegistryEntry(transformers, config, "villager-type", ZombieVillager.class, Registry.VILLAGER_TYPE, ZombieVillager::setVillagerType); + // Display ConfigData leftRotation = getQuaternion(config, "transformation.left-rotation"); ConfigData rightRotation = getQuaternion(config, "transformation.right-rotation"); @@ -499,6 +576,77 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptBoolean(transformers, config, "default-background", TextDisplay.class, TextDisplay::setDefaultBackground); addOptEnum(transformers, config, "alignment", TextDisplay.class, TextDisplay.TextAlignment.class, TextDisplay::setAlignment); + // Mob Goals + ConfigurationSection mobGoals = config.getConfigurationSection("mob-goals"); + if (mobGoals != null) { + if (mobGoals.getBoolean("remove-all")) + transformers.put(Mob.class, (Mob mob, SpellData data) -> Bukkit.getMobGoals().removeAllGoals(mob)); + + for (String string : mobGoals.getStringList("remove-types")) { + ConfigData typeData = ConfigDataUtil.getEnum(string, GoalType.class, null); + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + GoalType type = typeData.get(data); + if (type == null) return; + + Bukkit.getMobGoals().removeAllGoals(mob, type); + }); + } + + for (String string : mobGoals.getStringList("remove")) { + ConfigData keyData = ConfigDataUtil.getNamespacedKey(string, null); + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + NamespacedKey key = keyData.get(data); + if (key == null) return; + + Bukkit.getMobGoals().removeGoal(mob, GoalKey.of(Mob.class, key)); + }); + } + + for (String string : mobGoals.getStringList("remove-vanilla")) { + ConfigData stringData = ConfigDataUtil.getString(string); + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + String value = stringData.get(data); + if (value == null) return; + + GoalKey key = MagicSpells.getCustomGoalsManager().getVanillaGoal(value); + if (key == null) return; + + // We have to loop through because casting to parameter types is tricky. + // It loops through on each MobGoals#removeGoal call anyway. + for (Goal<@NotNull Mob> goal : Bukkit.getMobGoals().getAllGoals(mob)) { + if (!goal.getKey().equals(key)) continue; + Bukkit.getMobGoals().removeGoal(mob, goal); + } + }); + } + + for (Object object : mobGoals.getList("add", new ArrayList<>())) { + if (!(object instanceof Map map)) continue; + ConfigurationSection section = ConfigReaderUtil.mapToSection(map); + + ConfigData priorityData = ConfigDataUtil.getInteger(section, "priority", 0); + ConfigData goalNameData = ConfigDataUtil.getString(section, "goal", ""); + + ConfigurationSection goalSection = section.getConfigurationSection("data"); + if (goalSection == null) continue; + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + int priority = priorityData.get(data); + String goalName = goalNameData.get(data); + + CustomGoal goal = MagicSpells.getCustomGoalsManager().getGoal(goalName, mob, data); + if (goal == null) return; + + boolean success = goal.initialize(goalSection); + if (success) Bukkit.getMobGoals().addGoal(mob, priority, goal); + }); + } + } + + // Apply transformers for (EntityType entityType : EntityType.values()) { Class entityClass = entityType.getEntityClass(); if (entityClass == null) continue; @@ -508,6 +656,7 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { options.putAll(entityType, transformers.get(transformerType)); } + // Delayed Entity Data List delayedDataEntries = config.getList("delayed-entity-data"); if (delayedDataEntries == null || delayedDataEntries.isEmpty()) return; @@ -715,6 +864,11 @@ private void addOptDouble(Multimap, Transformer> transformers, C transformers.put(type, new TransformerImpl<>(supplier, setter, true)); } + private void addOptString(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { + ConfigData supplier = ConfigDataUtil.getString(config, name, null); + transformers.put(type, new TransformerImpl<>(supplier, setter, true)); + } + private > ConfigData addOptEnum(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, Class enumType, BiConsumer setter) { ConfigData supplier = ConfigDataUtil.getEnum(config, name, enumType, null); transformers.put(type, new TransformerImpl<>(supplier, setter, true)); diff --git a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java index 373d69139..bfbb57bfe 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java +++ b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java @@ -787,7 +787,10 @@ public static ConfigData getNamedTextColor(@NotNull Configuratio public static ConfigData getNamespacedKey(@NotNull ConfigurationSection config, @NotNull String path, @Nullable NamespacedKey def) { String value = config.getString(path); if (value == null) return data -> def; + return getNamespacedKey(value, def); + } + public static ConfigData getNamespacedKey(@NotNull String value, @Nullable NamespacedKey def) { NamespacedKey val = NamespacedKey.fromString(value.toLowerCase()); if (val != null) return data -> val; From f902fe9e893ba36c01688723b5b624ecff7bb889 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sat, 9 Aug 2025 14:59:19 +0200 Subject: [PATCH 11/14] feat: Add passengers --- .../listeners/MagicSpellListener.java | 25 +++++++++++++++++++ .../effecttypes/ArmorStandEffect.java | 6 ++++- .../effecttypes/EntityEffect.java | 6 ++++- .../magicspells/spells/buff/MinionSpell.java | 5 +++- .../spells/targeted/SpawnEntitySpell.java | 9 ++++--- .../nisovin/magicspells/util/EntityData.java | 16 ++++++++++++ .../com/nisovin/magicspells/util/Util.java | 7 ++++++ 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java b/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java index 06ea024a8..e4945103f 100644 --- a/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java +++ b/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java @@ -6,12 +6,17 @@ import org.bukkit.entity.LivingEntity; import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityRemoveEvent; +import org.bukkit.event.entity.EntityDismountEvent; +import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.inventory.InventoryPickupItemEvent; import com.nisovin.magicspells.Perm; import com.nisovin.magicspells.Spell; +import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.EntityData; import com.nisovin.magicspells.events.SpellTargetEvent; import com.nisovin.magicspells.zones.NoMagicZoneManager; import com.nisovin.magicspells.spelleffects.effecttypes.*; @@ -59,6 +64,26 @@ public void onEntityDamage(EntityDamageEvent event) { event.setCancelled(true); } + @EventHandler + public void onEntityRemove(EntityRemoveEvent event) { + Util.forEachPassenger(event.getEntity(), passenger -> { + PersistentDataContainer container = passenger.getPersistentDataContainer(); + if (!container.has(EntityData.MS_PASSENGER)) return; + + if (passenger.isPersistent()) { + container.remove(EntityData.MS_PASSENGER); + return; + } + + passenger.remove(); + }); + } + + @EventHandler + public void onEntityDismount(EntityDismountEvent event) { + event.getEntity().getPersistentDataContainer().remove(EntityData.MS_PASSENGER); + } + private boolean isMSEntity(Entity entity) { return entity.getScoreboardTags().contains(ArmorStandEffect.ENTITY_TAG) || entity.getScoreboardTags().contains(EntityEffect.ENTITY_TAG); } diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java index 3d360b6f5..b2862b7cd 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java @@ -7,6 +7,7 @@ import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.Name; +import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.EntityData; import com.nisovin.magicspells.util.config.ConfigData; @@ -64,7 +65,10 @@ protected ArmorStand playArmorStandEffectLocation(Location location, SpellData d stand.setItem(EquipmentSlot.HEAD, headItem); stand.setItem(EquipmentSlot.HAND, mainHandItem); stand.setItem(EquipmentSlot.OFF_HAND, offHandItem); - }, stand -> stand.setPersistent(false)); + }, stand -> { + stand.setPersistent(false); + Util.forEachPassenger(stand, e -> e.setPersistent(false)); + }); } } diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java index 6d3fbe1e3..339135c84 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java @@ -8,6 +8,7 @@ import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.Name; +import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.EntityData; @@ -45,7 +46,10 @@ protected Entity playEntityEffectLocation(Location location, SpellData data) { return entityData.spawn(location, data, entity -> { entity.addScoreboardTag(ENTITY_TAG); entity.setGravity(gravity.get(data)); - }, entity -> entity.setPersistent(false)); + }, entity -> { + entity.setPersistent(false); + Util.forEachPassenger(entity, e -> e.setPersistent(false)); + }); } @Override diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java index 84d634494..ba8361d9c 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java @@ -289,7 +289,10 @@ public boolean castBuff(SpellData data) { if (baby.get(data)) ageable.setBaby(); else ageable.setAdult(); }); - else minion = entityData.spawn(loc, data, mobClass, mob -> prepareMob(mob, target, data), null); + else minion = entityData.spawn(loc, data, mobClass, mob -> prepareMob(mob, target, data), mob -> { + mob.setPersistent(false); + Util.forEachPassenger(mob, e -> e.setPersistent(false)); + }); if (spawnSpell != null) { SpellData castData = data.builder().caster(target).target(minion).location(minion.getLocation()).recipient(null).build(); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java index 947784bab..ef439b783 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java @@ -438,10 +438,11 @@ private CastResult spawnMob(Location source, SpellData data) { } SpellData finalData = data; - Entity entity = entityData.spawn(loc, data, - mob -> prepMob(mob, finalData), - mob -> mob.setPersistent(!removeMob) - ); + Entity entity = entityData.spawn(loc, data, mob -> prepMob(mob, finalData), mob -> { + if (!removeMob) return; + mob.setPersistent(false); + Util.forEachPassenger(mob, e -> e.setPersistent(false)); + }); if (entity == null) return noTarget(data); UUID uuid = entity.getUniqueId(); diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index be34fa9ff..e99a09d0d 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -36,6 +36,7 @@ import org.bukkit.inventory.EntityEquipment; import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeModifier; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.entity.minecart.CommandMinecart; import org.bukkit.configuration.ConfigurationSection; @@ -56,6 +57,8 @@ public class EntityData { + public static final NamespacedKey MS_PASSENGER = new NamespacedKey(MagicSpells.getInstance(), "entity_passenger"); + private final Multimap> options = MultimapBuilder.enumKeys(EntityType.class).arrayListValues().build(); private final List delayedEntityData = new ArrayList<>(); @@ -576,6 +579,19 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptBoolean(transformers, config, "default-background", TextDisplay.class, TextDisplay::setDefaultBackground); addOptEnum(transformers, config, "alignment", TextDisplay.class, TextDisplay.TextAlignment.class, TextDisplay::setAlignment); + // Passengers + for (Object object : config.getList("passengers", new ArrayList<>())) { + if (!(object instanceof Map map)) continue; + EntityData passengerData = new EntityData(ConfigReaderUtil.mapToSection(map)); + + transformers.put(Entity.class, (Entity entity, SpellData data) -> { + passengerData.spawn(entity.getLocation(), data, passenger -> { + entity.addPassenger(passenger); + passenger.getPersistentDataContainer().set(MS_PASSENGER, PersistentDataType.BOOLEAN, true); + }); + }); + } + // Mob Goals ConfigurationSection mobGoals = config.getConfigurationSection("mob-goals"); if (mobGoals != null) { diff --git a/core/src/main/java/com/nisovin/magicspells/util/Util.java b/core/src/main/java/com/nisovin/magicspells/util/Util.java index 758fc3238..affe054b8 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/Util.java +++ b/core/src/main/java/com/nisovin/magicspells/util/Util.java @@ -582,6 +582,13 @@ public static void forEachPlayerOnline(Consumer consumer) { Bukkit.getOnlinePlayers().forEach(consumer); } + public static void forEachPassenger(Entity entity, Consumer consumer) { + for (Entity passenger : entity.getPassengers()) { + forEachPassenger(passenger, consumer); + consumer.accept(passenger); + } + } + public static > C getMaterialList(List strings, Supplier supplier) { C ret = supplier.get(); strings.forEach(string -> ret.add(getMaterial(string))); From ea2a0af0c5180333e7e6f33635df1ad6c0e1be3c Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Sun, 10 Aug 2025 02:48:39 +0200 Subject: [PATCH 12/14] feat: Expand scoreboard tags operations --- .../nisovin/magicspells/util/EntityData.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index e99a09d0d..042724fd3 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -149,9 +149,36 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptVector(transformers, config, "velocity", Entity.class, Entity::setVelocity); - for (String tagString : config.getStringList("scoreboard-tags")) { - ConfigData tag = ConfigDataUtil.getString(tagString); - transformers.put(Entity.class, (Entity entity, SpellData data) -> entity.addScoreboardTag(tag.get(data))); + for (Object object : config.getList("scoreboard-tags", new ArrayList<>())) { + switch (object) { + case String string -> { + ConfigData tag = ConfigDataUtil.getString(string); + transformers.put(Entity.class, (Entity entity, SpellData data) -> entity.addScoreboardTag(tag.get(data))); + } + case Map map -> { + ConfigurationSection section = ConfigReaderUtil.mapToSection(map); + + ConfigData operation = ConfigDataUtil.getEnum(section, "operation", EntityTagOperation.class, EntityTagOperation.ADD); + ConfigData tagData = ConfigDataUtil.getString(section, "tag", null); + + transformers.put(Entity.class, (Entity entity, SpellData data) -> { + switch (operation.get(data)) { + case ADD -> { + String tag = tagData.get(data); + if (tag == null) break; + entity.addScoreboardTag(tag); + } + case REMOVE -> { + String tag = tagData.get(data); + if (tag == null) break; + entity.removeScoreboardTag(tag); + } + case CLEAR -> entity.getScoreboardTags().clear(); + } + }); + } + default -> {} + } } // Ageable @@ -1328,6 +1355,12 @@ public ConfigData getProfession() { return profession; } + private enum EntityTagOperation { + ADD, + REMOVE, + CLEAR, + } + private record DelayedEntityData(EntityData data, ConfigData delay, ConfigData interval, ConfigData iterations) { } From ef8fa0070d643ea08e26c922be2edaa1932e7b65 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 4 Aug 2025 04:31:22 +0200 Subject: [PATCH 13/14] feat: Add EntityDataApplySpell --- .../spells/targeted/EntityDataApplySpell.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java new file mode 100644 index 000000000..dd7b16cc0 --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java @@ -0,0 +1,37 @@ +package com.nisovin.magicspells.spells.targeted; + +import org.bukkit.entity.LivingEntity; + +import com.nisovin.magicspells.util.*; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.spells.TargetedEntitySpell; + +public class EntityDataApplySpell extends TargetedSpell implements TargetedEntitySpell { + + private final EntityData entityData; + + public EntityDataApplySpell(MagicConfig config, String spellName) { + super(config, spellName); + + entityData = new EntityData(getConfigSection("entity")); + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedEntity(data); + if (info.noTarget()) return noTarget(info); + + return castAtEntity(info.spellData()); + } + + @Override + public CastResult castAtEntity(SpellData data) { + if (!data.hasTarget()) return noTarget(data); + + entityData.apply(data.target(), data); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +} From dc77afdae707bba63da134c4bc674bf647839c36 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 11 Aug 2025 01:47:07 +0200 Subject: [PATCH 14/14] feat: Add EntityRemoveSpell --- .../spells/targeted/EntityRemoveSpell.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java new file mode 100644 index 000000000..f834c85cf --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java @@ -0,0 +1,34 @@ +package com.nisovin.magicspells.spells.targeted; + +import org.bukkit.entity.Player; +import org.bukkit.entity.LivingEntity; + +import com.nisovin.magicspells.util.*; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.spells.TargetedEntitySpell; + +public class EntityRemoveSpell extends TargetedSpell implements TargetedEntitySpell { + + public EntityRemoveSpell(MagicConfig config, String spellName) { + super(config, spellName); + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedEntity(data); + if (info.noTarget()) return noTarget(info); + + return castAtEntity(info.spellData()); + } + + @Override + public CastResult castAtEntity(SpellData data) { + if (!data.hasTarget() || data.target() instanceof Player) return noTarget(data); + + data.target().remove(); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +}