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 1ebd8c860..caf4d503b 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; @@ -23,6 +24,7 @@ public class ArmorStandEffect extends SpellEffect { private EntityData entityData; private ConfigData gravity; + private ConfigData disableSlots; private ItemStack headItem; private ItemStack offhandItem; @@ -36,6 +38,7 @@ protected void loadFromConfig(ConfigurationSection config) { entityData = new EntityData(section); gravity = ConfigDataUtil.getBoolean(section, "gravity", false); + disableSlots = ConfigDataUtil.getBoolean(section, "disable-slots", true); MagicItem item = MagicItems.getMagicItemFromString(section.getString("head")); if (item != null) headItem = item.getItemStack(); @@ -54,11 +57,15 @@ protected ArmorStand playArmorStandEffectLocation(Location location, SpellData d stand.addScoreboardTag(ENTITY_TAG); stand.setGravity(gravity.get(data)); + if (disableSlots.get(data)) stand.setDisabledSlots(EquipmentSlot.values()); 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 9ad93d79f..23ea4bf96 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 @@ -287,7 +287,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/ApplyEntityDataSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ApplyEntityDataSpell.java new file mode 100644 index 000000000..38a2f4998 --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ApplyEntityDataSpell.java @@ -0,0 +1,35 @@ +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 ApplyEntityDataSpell extends TargetedSpell implements TargetedEntitySpell { + + private final EntityData entityData; + + public ApplyEntityDataSpell(MagicConfig config, String spellName) { + super(config, spellName); + + entityData = new EntityData(getConfigSection("entity"), true); + } + + @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) { + entityData.apply(data.target(), data); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +} 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..59ecc10ef --- /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.target() instanceof Player) return noTarget(data); + + data.target().remove(); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +} 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 a93aa579c..274b43335 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,10 +439,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 a55aff02f..341e4fec8 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,30 +15,44 @@ 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.SkinParts; +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; import org.bukkit.util.EulerAngle; +import org.bukkit.inventory.MainHand; import org.bukkit.attribute.Attribute; import org.bukkit.inventory.ItemStack; import org.bukkit.util.Transformation; import org.bukkit.potion.PotionEffect; import org.bukkit.block.data.BlockData; +import org.bukkit.profile.PlayerTextures; import org.bukkit.attribute.Attributable; import org.bukkit.inventory.EquipmentSlot; 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; +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.world.WeatheringCopperState; +import io.papermc.paper.datacomponent.item.ResolvableProfile; 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; @@ -51,6 +62,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<>(); @@ -116,6 +129,7 @@ public EntityData(ConfigurationSection config) { this(config, false); } + @SuppressWarnings("UnstableApiUsage") public EntityData(ConfigurationSection config, boolean forceOptional) { entityType = ConfigDataUtil.getEntityType(config, "entity", null); @@ -129,14 +143,49 @@ 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, "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); + addOptEnum(transformers, config, "visual-fire", Entity.class, TriState.class, Entity::setVisualFire); + + 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")) { - 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 @@ -169,6 +218,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 +238,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 +250,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) -> { @@ -207,6 +268,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); @@ -215,6 +279,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), @@ -227,12 +318,34 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "cat-variant", "type" ); + // Chicken + addOptRegistryEntry(transformers, config, "chicken-variant", Chicken.class, RegistryKey.CHICKEN_VARIANT, Chicken::setVariant); + + // Copper Golem + addOptEnum(transformers, config, "copper-golem.weathering-state", CopperGolem.class, WeatheringCopperState.class, CopperGolem::setWeatheringState); + addOptLong(transformers, config, "copper-golem.oxidizing", CopperGolem.class, (golem, next) -> { + long time = golem.getWorld().getGameTime() + next; + golem.setOxidizing(CopperGolem.Oxidizing.atTime(time)); + }); + addOptString(transformers, config, "copper-golem.oxidizing", CopperGolem.class, (golem, value) -> { + switch (value) { + case "unset" -> golem.setOxidizing(CopperGolem.Oxidizing.unset()); + case "waxed" -> golem.setOxidizing(CopperGolem.Oxidizing.waxed()); + } + }); + // CollarColorable fallback( key -> addOptEnum(transformers, config, key, CollarColorable.class, DyeColor.class, CollarColorable::setCollarColor), "collar-color", "color" ); + // CommandMinecart + addOptString(transformers, config, "command", CommandMinecart.class, CommandMinecart::setCommand); + + // Cow + addOptRegistryEntry(transformers, config, "cow-variant", Cow.class, RegistryKey.COW_VARIANT, Cow::setVariant); + // ChestedHorse chested = addBoolean(transformers, config, "chested", false, ChestedHorse.class, ChestedHorse::setCarryingChest, forceOptional); @@ -251,18 +364,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, RegistryKey.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), @@ -300,6 +432,57 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "llama-decor", "material" ); + // Mannequin + addOptBoolean(transformers, config, "mannequin.immovable", Mannequin.class, Mannequin::setImmovable); + + addOptSkinParts(transformers, config, "mannequin.skin-parts.cape", SkinParts.Mutable::setCapeEnabled); + addOptSkinParts(transformers, config, "mannequin.skin-parts.jacket", SkinParts.Mutable::setJacketEnabled); + addOptSkinParts(transformers, config, "mannequin.skin-parts.left-sleeve", SkinParts.Mutable::setLeftSleeveEnabled); + addOptSkinParts(transformers, config, "mannequin.skin-parts.right-sleeve", SkinParts.Mutable::setRightSleeveEnabled); + addOptSkinParts(transformers, config, "mannequin.skin-parts.left-pants", SkinParts.Mutable::setLeftPantsEnabled); + addOptSkinParts(transformers, config, "mannequin.skin-parts.right-pants", SkinParts.Mutable::setRightPantsEnabled); + addOptSkinParts(transformers, config, "mannequin.skin-parts.hats", SkinParts.Mutable::setHatsEnabled); + + addComponent(transformers, config, "mannequin.description", Mannequin.class, Mannequin::setDescription, null, forceOptional); + + addOptEnum(transformers, config, "mannequin.main-hand", Mannequin.class, MainHand.class, Mannequin::setMainHand); + + // noinspection PatternValidation + ConfigData mannequinName = ConfigDataUtil.getString(config, "mannequin.profile.name", null); + ConfigData mannequinId = ConfigDataUtil.getUniqueID(config, "mannequin.profile.uuid", null); + ConfigData mannequinBody = ConfigDataUtil.getNamespacedKey(config, "mannequin.profile.body", null); + ConfigData mannequinCape = ConfigDataUtil.getNamespacedKey(config, "mannequin.profile.cape", null); + ConfigData mannequinElytra = ConfigDataUtil.getNamespacedKey(config, "mannequin.profile.elytra", null); + ConfigData mannequinModel = ConfigDataUtil.getEnum(config, "mannequin.profile.model", PlayerTextures.SkinModel.class, null); + ConfigData mannequinProfile = data -> { + try { + // noinspection PatternValidation + return ResolvableProfile.resolvableProfile() + .name(mannequinName.get(data)) + .uuid(mannequinId.get(data)) + .skinPatch(builder -> { + builder.body(mannequinBody.get(data)); + builder.cape(mannequinCape.get(data)); + builder.elytra(mannequinElytra.get(data)); + builder.model(mannequinModel.get(data)); + }) + .build(); + } catch (IllegalArgumentException e) { + return null; + } + }; + + transformers.put(Mannequin.class, new TransformerImpl<>(mannequinProfile, Mannequin::setProfile, true)); + + // 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), @@ -320,6 +503,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); @@ -329,6 +519,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( @@ -365,6 +560,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), @@ -372,6 +570,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); @@ -379,6 +580,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"); @@ -469,6 +674,90 @@ 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) { + 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; @@ -478,6 +767,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; @@ -671,6 +961,11 @@ private void addOptInteger(Multimap, Transformer> transformers, transformers.put(type, new TransformerImpl<>(supplier, setter, true)); } + private void addOptLong(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { + ConfigData supplier = ConfigDataUtil.getLong(config, name); + transformers.put(type, new TransformerImpl<>(supplier, setter, true)); + } + private void addOptFloat(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { ConfigData supplier = ConfigDataUtil.getFloat(config, name); transformers.put(type, new TransformerImpl<>(supplier, setter, true)); @@ -681,6 +976,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)); @@ -710,6 +1010,18 @@ private void addOptComponent(Multimap, Transformer> transformers transformers.put(type, new TransformerImpl<>(supplier, setter, true)); } + private void addComponent(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter, Component def, boolean forceOptional) { + ConfigData supplier; + + if (forceOptional) { + supplier = ConfigDataUtil.getComponent(config, name, null); + transformers.put(type, new TransformerImpl<>(supplier, setter, true)); + } else { + supplier = ConfigDataUtil.getComponent(config, name, def); + transformers.put(type, new TransformerImpl<>(supplier, setter)); + } + } + private ConfigData addOptBlockData(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { ConfigData supplier = ConfigDataUtil.getBlockData(config, name, null); transformers.put(type, new TransformerImpl<>(supplier, setter, true)); @@ -758,13 +1070,21 @@ private void addOptEquipment(Multimap, Transfo }); } - private void addOptEquipmentDropChance(Multimap, Transformer> transformers, ConfigurationSection config, String name, EquipmentSlot slot) { + private void addOptEquipmentDropChance(Multimap, Transformer> transformers, ConfigurationSection config, String name, EquipmentSlot slot) { addOptFloat(transformers, config, name, Mob.class, (entity, chance) -> { EntityEquipment equipment = entity.getEquipment(); equipment.setDropChance(slot, chance); }); } + private void addOptSkinParts(Multimap, Transformer> transformers, ConfigurationSection config, String name, BiConsumer setter) { + addOptBoolean(transformers, config, name, Mannequin.class, (mannequin, bool) -> { + SkinParts.Mutable parts = mannequin.getSkinParts(); + setter.accept(parts, bool); + mannequin.setSkinParts(parts); + }); + } + private ConfigData<@NotNull R> addOptRegistryEntry(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, RegistryKey<@NotNull R> key, BiConsumer setter) { return addOptRegistryEntry(transformers, config, name, type, RegistryAccess.registryAccess().getRegistry(key), setter); } @@ -1124,6 +1444,12 @@ public ConfigData getProfession() { return profession; } + private enum EntityTagOperation { + ADD, + REMOVE, + CLEAR, + } + private record DelayedEntityData(EntityData data, ConfigData delay, ConfigData interval, ConfigData iterations) { } 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 335d7b1bc..63f9ac42b 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))); 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..efffade4d 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 @@ -1,6 +1,7 @@ package com.nisovin.magicspells.util.config; import java.util.List; +import java.util.UUID; import java.util.function.Function; import org.jetbrains.annotations.NotNull; @@ -506,10 +507,16 @@ private static boolean parseBoolean(@Nullable String value, boolean def) { @NotNull public static > ConfigData getEnum(@NotNull ConfigurationSection config, - @NotNull String path, + @NotNull String path, + @NotNull Class type, + @Nullable T def) { + return ConfigDataUtil.getEnum(config.getString(path), type, def); + } + + @NotNull + public static > ConfigData getEnum(@Nullable String value, @NotNull Class type, @Nullable T def) { - String value = config.getString(path); if (value == null) return data -> def; try { @@ -781,7 +788,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; @@ -797,6 +807,22 @@ public static ConfigData getNamespacedKey(@NotNull ConfigurationS }; } + public static ConfigData getUniqueID(@NotNull ConfigurationSection config, @NotNull String path, @Nullable UUID def) { + ConfigData supplier = getString(config, path, null); + + return (VariableConfigData) data -> { + String uuid = supplier.get(data); + if (uuid == null) return def; + + try { + return UUID.fromString(uuid); + } + catch (IllegalArgumentException e) { + return def; + } + }; + } + public static ConfigData getAngle(@NotNull ConfigurationSection config, @NotNull String path, @Nullable Angle def) { if (config.isInt(path) || config.isLong(path) || config.isDouble(path)) { float value = (float) config.getDouble(path);