diff --git a/pom.xml b/pom.xml index e29d7d061..3198bfad0 100644 --- a/pom.xml +++ b/pom.xml @@ -419,6 +419,13 @@ 2.0.0-SNAPSHOT provided + + + de.oliver + FancyHolograms + 2.4.1 + provided + diff --git a/src/main/java/world/bentobox/bentobox/api/hooks/NPCHook.java b/src/main/java/world/bentobox/bentobox/api/hooks/NPCHook.java new file mode 100644 index 000000000..6a848215f --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/hooks/NPCHook.java @@ -0,0 +1,41 @@ +package world.bentobox.bentobox.api.hooks; + +import java.util.List; +import java.util.Map; + +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +import de.oliver.fancynpcs.api.Npc; +import lol.pyr.znpcsplus.api.npc.NpcEntry; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; + +/** + * NPC Hooks + * @author tastybento + * @since 3.2.0 + */ +public abstract class NPCHook extends Hook { + + protected NPCHook(@NonNull String pluginName, @NonNull Material icon) { + super(pluginName, icon); + } + + public abstract boolean spawnNpc(String yaml, Location pos) throws InvalidConfigurationException; + + public abstract Map> getNpcsInArea(World world, + List vectorsToCopy, @Nullable Vector origin); + + /** + * Remove all NPCs in chunk + * @param chunk chunk + */ + public abstract void removeNPCsInChunk(Chunk chunk); + +} diff --git a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java index 6d678bd08..5522ea867 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java @@ -12,25 +12,20 @@ import java.util.Optional; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.block.Banner; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.CreatureSpawner; import org.bukkit.block.Sign; +import org.bukkit.block.data.Attachable; import org.bukkit.block.sign.Side; -import org.bukkit.entity.AbstractHorse; -import org.bukkit.entity.Ageable; -import org.bukkit.entity.ChestedHorse; import org.bukkit.entity.Entity; -import org.bukkit.entity.Horse; import org.bukkit.entity.Player; -import org.bukkit.entity.Tameable; -import org.bukkit.entity.Villager; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; -import org.bukkit.material.Attachable; -import org.bukkit.material.Colorable; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.BoundingBox; import org.bukkit.util.Vector; @@ -57,6 +52,10 @@ import world.bentobox.bentobox.hooks.ZNPCsPlusHook; */ public class BlueprintClipboard { + /** + * Used to filter out hidden DisplayEntity armor stands when copying + */ + private static final NamespacedKey KEY = new NamespacedKey(BentoBox.getInstance(), "associatedDisplayEntity"); private @Nullable Blueprint blueprint; private @Nullable Location pos1; private @Nullable Location pos2; @@ -74,6 +73,7 @@ public class BlueprintClipboard { private Optional npc; private Optional znpc; + /** * Create a clipboard for blueprint * @param blueprint - the blueprint to load into the clipboard @@ -148,6 +148,7 @@ public class BlueprintClipboard { // Add all the citizens for the area in one go. This is pretty fast. bpEntities.putAll(npc.get().getNpcsInArea(world, vectorsToCopy, origin)); } + // ZNPCsPlus NPCs if (znpc.isPresent()) { bpEntities.putAll(znpc.get().getNpcsInArea(world, vectorsToCopy, origin)); } @@ -162,9 +163,9 @@ public class BlueprintClipboard { List ents = world.getEntities().stream() .filter(Objects::nonNull) .filter(e -> !(e instanceof Player)) - .filter(e -> new Vector(Math.rint(e.getLocation().getX()), - Math.rint(e.getLocation().getY()), - Math.rint(e.getLocation().getZ())).equals(v)) + .filter(e -> !e.getPersistentDataContainer().has(KEY, PersistentDataType.STRING)) // Do not copy hidden display entities + .filter(e -> new Vector(e.getLocation().getBlockX(), e.getLocation().getBlockY(), + e.getLocation().getBlockZ()).equals(v)) .toList(); if (copyBlock(v.toLocation(world), copyAir, copyBiome, ents)) { count++; @@ -230,7 +231,6 @@ public class BlueprintClipboard { if (!copyAir && block.getType().equals(Material.AIR) && !ents.isEmpty()) { return true; } - BlueprintBlock b = bluePrintBlock(pos, block, copyBiome); if (b != null) { this.bpBlocks.put(pos, b); @@ -256,7 +256,7 @@ public class BlueprintClipboard { } } // Set block data - if (blockState.getData() instanceof Attachable) { + if (blockState.getBlockData() instanceof Attachable) { // Placeholder for attachment bpBlocks.put(pos, new BlueprintBlock("minecraft:air")); bpAttachable.put(pos, b); @@ -273,7 +273,6 @@ public class BlueprintClipboard { } } } - // Chests if (blockState instanceof InventoryHolder ih) { b.setInventory(new HashMap<>()); @@ -284,11 +283,9 @@ public class BlueprintClipboard { } } } - if (blockState instanceof CreatureSpawner spawner) { b.setCreatureSpawner(getSpawner(spawner)); } - // Banners if (blockState instanceof Banner banner) { b.setBannerPatterns(banner.getPatterns()); @@ -317,62 +314,15 @@ public class BlueprintClipboard { private List setEntities(List ents) { List bpEnts = new ArrayList<>(); for (Entity entity : ents) { - BlueprintEntity bpe = new BlueprintEntity(); - - bpe.setType(entity.getType()); - bpe.setCustomName(entity.getCustomName()); - if (entity instanceof Villager villager) { - setVillager(villager, bpe); - } - if (entity instanceof Colorable c && c.getColor() != null) { - bpe.setColor(c.getColor()); - } - if (entity instanceof Tameable tameable) { - bpe.setTamed(tameable.isTamed()); - } - if (entity instanceof ChestedHorse chestedHorse) { - bpe.setChest(chestedHorse.isCarryingChest()); - } - // Only set if child. Most animals are adults - if (entity instanceof Ageable ageable && !ageable.isAdult()) { - bpe.setAdult(false); - } - if (entity instanceof AbstractHorse horse) { - bpe.setDomestication(horse.getDomestication()); - bpe.setInventory(new HashMap<>()); - for (int i = 0; i < horse.getInventory().getSize(); i++) { - ItemStack item = horse.getInventory().getItem(i); - if (item != null) { - bpe.getInventory().put(i, item); - } - } - } - - if (entity instanceof Horse horse) { - bpe.setStyle(horse.getStyle()); - } - + BlueprintEntity bpe = new BlueprintEntity(entity); // Mythic mob check mmh.filter(mm -> mm.isMythicMob(entity)).map(mm -> mm.getMythicMob(entity)) .ifPresent(bpe::setMythicMobsRecord); - bpEnts.add(bpe); } return bpEnts; } - /** - * Set the villager stats - * @param v - villager - * @param bpe - Blueprint Entity - */ - private void setVillager(Villager v, BlueprintEntity bpe) { - bpe.setExperience(v.getVillagerExperience()); - bpe.setLevel(v.getVillagerLevel()); - bpe.setProfession(v.getProfession()); - bpe.setVillagerType(v.getVillagerType()); - } - /** * @return the origin */ diff --git a/src/main/java/world/bentobox/bentobox/blueprints/DisplayListener.java b/src/main/java/world/bentobox/bentobox/blueprints/DisplayListener.java new file mode 100644 index 000000000..efa1dccf3 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/blueprints/DisplayListener.java @@ -0,0 +1,45 @@ +package world.bentobox.bentobox.blueprints; + +import java.util.UUID; + +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Display; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.persistence.PersistentDataType; + +import world.bentobox.bentobox.BentoBox; + +/** + * Provides a listener for the Display Objects pasted when a hologram is interacted with + * https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/event/player/PlayerInteractAtEntityEvent.html + */ +public class DisplayListener implements Listener { + + @EventHandler + public void onPlayerInteractEntity(PlayerInteractAtEntityEvent event) { + if (event.getRightClicked() instanceof ArmorStand) { + ArmorStand armorStand = (ArmorStand) event.getRightClicked(); + NamespacedKey key = new NamespacedKey(BentoBox.getInstance(), "associatedDisplayEntity"); + + if (armorStand.getPersistentDataContainer().has(key, PersistentDataType.STRING)) { + String displayEntityUUID = armorStand.getPersistentDataContainer().get(key, PersistentDataType.STRING); + + // Fetch the associated DisplayEntity by UUID + World world = armorStand.getWorld(); + world.getEntitiesByClass(Display.class).stream() + .filter(e -> e.getUniqueId().equals(UUID.fromString(displayEntityUUID))).findFirst() + .ifPresent(e -> { + event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.BLOCK_GLASS_BREAK, 1F, + 1F); + e.remove(); + + }); + } + } + } +} diff --git a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java index 2ce285fee..e41ebcc9b 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java @@ -1,78 +1,234 @@ package world.bentobox.bentobox.blueprints.dataobjects; +import java.util.HashMap; import java.util.Map; +import org.bukkit.Bukkit; +import org.bukkit.Color; import org.bukkit.DyeColor; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; import org.bukkit.entity.AbstractHorse; import org.bukkit.entity.Ageable; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.BlockDisplay; import org.bukkit.entity.ChestedHorse; +import org.bukkit.entity.Display; +import org.bukkit.entity.Display.Billboard; +import org.bukkit.entity.Display.Brightness; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Horse; import org.bukkit.entity.Horse.Style; +import org.bukkit.entity.ItemDisplay; +import org.bukkit.entity.ItemDisplay.ItemDisplayTransform; import org.bukkit.entity.Tameable; +import org.bukkit.entity.TextDisplay; +import org.bukkit.entity.TextDisplay.TextAlignment; import org.bukkit.entity.Villager; import org.bukkit.entity.Villager.Profession; import org.bukkit.inventory.ItemStack; import org.bukkit.material.Colorable; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; import com.google.gson.annotations.Expose; +import world.bentobox.bentobox.BentoBox; + /** * @author tastybento * @since 1.5.0 */ public class BlueprintEntity { - // Npc storage - @Expose - private String npc; - // MythicMobs storage public record MythicMobRecord(String type, String displayName, double level, float power, String stance) { } - // GSON can serialize records, but the record class needs to be know in advance. So this breaks out the record entries - @Expose - String MMtype; - @Expose - Double MMLevel; - @Expose - String MMStance; - @Expose - Float MMpower; + /** + * Item Display Entity store + * @since 3.2.0 + */ + public record ItemDispRec(@Expose ItemStack item, @Expose ItemDisplayTransform itemDispTrans) {} - @Expose - private DyeColor color; - @Expose - private EntityType type; - @Expose - private String customName; - @Expose - private Boolean tamed; - @Expose - private Boolean chest; + /** + * Display Entity store + * @since 3.2.0 + */ + public record DisplayRec(@Expose Billboard billboard, @Expose Brightness brightness, @Expose float height, + @Expose float width, @Expose Color glowColorOverride, @Expose int interpolationDelay, + @Expose int interpolationDuration, @Expose float shadowRadius, @Expose float shadowStrength, + @Expose int teleportDuration, @Expose Transformation transformation, @Expose float range) { + } + + /** + * TextDisplay entity store + * @since 3.2.0 + */ + public record TextDisplayRec(@Expose String text, @Expose TextAlignment alignment, @Expose Color bgColor, + @Expose BlockFace face, @Expose int lWidth, @Expose byte opacity, @Expose boolean isShadowed, + @Expose boolean isSeeThrough, @Expose boolean isDefaultBg) { + } @Expose private Boolean adult; @Expose + public BlueprintBlock blockDisp; + + @Expose + private Boolean chest; + @Expose + private DyeColor color; + @Expose + private String customName; + @Expose + public DisplayRec displayRec; + @Expose private Integer domestication; @Expose - private Map inventory; - @Expose - private Style style; - @Expose - private Integer level; - @Expose - private Profession profession; - @Expose private Integer experience; @Expose + private Map inventory; + @Expose + public ItemDispRec itemDisp; + @Expose + private Integer level; + @Expose + Double MMLevel; + @Expose + Float MMpower; + @Expose + String MMStance; + // GSON can serialize records, but the record class needs to be know in advance. So this breaks out the record entries + @Expose + String MMtype; + // Npc storage + @Expose + private String npc; + @Expose + private Profession profession; + @Expose + private Style style; + + @Expose + private Boolean tamed; + + @Expose + public TextDisplayRec textDisp; + + @Expose + private EntityType type; + @Expose private Villager.Type villagerType; + // Position within the block + @Expose + private double x; + @Expose + private double y; + @Expose + private double z; + @Expose + private boolean glowing; + @Expose + private boolean gravity; + @Expose + private boolean visualFire; + @Expose + private boolean silent; + @Expose + private boolean invulnerable; + @Expose + private int fireTicks; /** + * Serializes an entity to a Blueprint Entity + * @param entity entity to serialize + * @since 3.2.0 + */ + public BlueprintEntity(Entity entity) { + this.setType(entity.getType()); + this.setCustomName(entity.getCustomName()); + this.setGlowing(entity.isGlowing()); + this.setGravity(entity.hasGravity()); + this.setVisualFire(entity.isVisualFire()); + this.setSilent(entity.isSilent()); + this.setInvulnerable(entity.isInvulnerable()); + this.setFireTicks(entity.getFireTicks()); + + if (entity instanceof Villager villager) { + configVillager(villager); + } + if (entity instanceof Colorable c && c.getColor() != null) { + this.setColor(c.getColor()); + } + if (entity instanceof Tameable tameable) { + this.setTamed(tameable.isTamed()); + } + if (entity instanceof ChestedHorse chestedHorse) { + this.setChest(chestedHorse.isCarryingChest()); + } + // Only set if child. Most animals are adults + if (entity instanceof Ageable ageable && !ageable.isAdult()) { + this.setAdult(false); + } + if (entity instanceof AbstractHorse horse) { + this.setDomestication(horse.getDomestication()); + this.setInventory(new HashMap<>()); + for (int i = 0; i < horse.getInventory().getSize(); i++) { + ItemStack item = horse.getInventory().getItem(i); + if (item != null) { + this.getInventory().put(i, item); + } + } + } + + if (entity instanceof Horse horse) { + this.setStyle(horse.getStyle()); + } + + // Display entities + if (entity instanceof Display disp) { + this.storeDisplay(disp); + } + + } + + /** + * Makes a blank BlueprintEntity + */ + public BlueprintEntity() { + // Blank constructor + } + + /** + * Set the villager stats + * @param v - villager + * @param bpe - Blueprint Entity + */ + private void configVillager(Villager v) { + this.setExperience(v.getVillagerExperience()); + this.setLevel(v.getVillagerLevel()); + this.setProfession(v.getProfession()); + this.setVillagerType(v.getVillagerType()); + } + + /** + * Adjusts the entity according to how it was stored * @since 1.8.0 */ public void configureEntity(Entity e) { + // Set the general states + e.setGlowing(glowing); + e.setGravity(gravity); + e.setVisualFire(visualFire); + e.setSilent(silent); + e.setInvulnerable(invulnerable); + e.setFireTicks(fireTicks); + if (e instanceof Villager villager) { setVillager(villager); } @@ -102,78 +258,8 @@ public class BlueprintEntity { if (style != null && e instanceof Horse horse) { horse.setStyle(style); } - } - - /** - * @param v - villager - * @since 1.16.0 - */ - private void setVillager(Villager v) { - v.setProfession(profession == null ? Profession.NONE : profession); - v.setVillagerExperience(experience == null ? 0 : experience); - v.setVillagerLevel(level == null ? 0 : level); - v.setVillagerType(villagerType == null ? Villager.Type.PLAINS : villagerType); - } - - /** - * @return the color - */ - public DyeColor getColor() { - return color; - } - /** - * @param color the color to set - */ - public void setColor(DyeColor color) { - this.color = color; - } - /** - * @return the type - */ - public EntityType getType() { - return type; - } - /** - * @param type the type to set - */ - public void setType(EntityType type) { - this.type = type; - } - /** - * @return the customName - */ - public String getCustomName() { - return customName; - } - /** - * @param customName the customName to set - */ - public void setCustomName(String customName) { - this.customName = customName; - } - /** - * @return the tamed - */ - public Boolean getTamed() { - return tamed; - } - /** - * @param tamed the tamed to set - */ - public void setTamed(Boolean tamed) { - this.tamed = tamed; - } - /** - * @return the chest - */ - public Boolean getChest() { - return chest; - } - /** - * @param chest the chest to set - */ - public void setChest(Boolean chest) { - this.chest = chest; + // Shift to the in-block location (remove the 0.5 that the location serializer used) + e.getLocation().add(new Vector(x - 0.5D, y, z - 0.5D)); } /** * @return the adult @@ -182,10 +268,22 @@ public class BlueprintEntity { return adult; } /** - * @param adult the adult to set + * @return the chest */ - public void setAdult(Boolean adult) { - this.adult = adult; + public Boolean getChest() { + return chest; + } + /** + * @return the color + */ + public DyeColor getColor() { + return color; + } + /** + * @return the customName + */ + public String getCustomName() { + return customName; } /** * @return the domestication @@ -194,10 +292,10 @@ public class BlueprintEntity { return domestication; } /** - * @param domestication the domestication to set + * @return the experience */ - public void setDomestication(int domestication) { - this.domestication = domestication; + public Integer getExperience() { + return experience; } /** * @return the inventory @@ -206,10 +304,31 @@ public class BlueprintEntity { return inventory; } /** - * @param inventory the inventory to set + * @return the level */ - public void setInventory(Map inventory) { - this.inventory = inventory; + public Integer getLevel() { + return level; + } + /** + * @return the mythicMobsRecord + */ + public MythicMobRecord getMythicMobsRecord() { + if (this.MMtype == null || this.MMLevel == null || this.MMpower == null || this.MMStance == null) { + return null; + } + return new MythicMobRecord(this.MMtype, this.getCustomName(), this.MMLevel, this.MMpower, this.MMStance); + } + /** + * @return the npc + */ + public String getNpc() { + return npc; + } + /** + * @return the profession + */ + public Profession getProfession() { + return profession; } /** * @return the style @@ -217,53 +336,19 @@ public class BlueprintEntity { public Style getStyle() { return style; } + /** - * @param style the style to set + * @return the tamed */ - public void setStyle(Style style) { - this.style = style; + public Boolean getTamed() { + return tamed; } /** - * @return the level + * @return the type */ - public Integer getLevel() { - return level; - } - - /** - * @param level the level to set - */ - public void setLevel(Integer level) { - this.level = level; - } - - /** - * @return the profession - */ - public Profession getProfession() { - return profession; - } - - /** - * @param profession the profession to set - */ - public void setProfession(Profession profession) { - this.profession = profession; - } - - /** - * @return the experience - */ - public Integer getExperience() { - return experience; - } - - /** - * @param experience the experience to set - */ - public void setExperience(Integer experience) { - this.experience = experience; + public EntityType getType() { + return type; } /** @@ -274,10 +359,93 @@ public class BlueprintEntity { } /** - * @param villagerType the villagerType to set + * @param adult the adult to set */ - public void setVillagerType(Villager.Type villagerType) { - this.villagerType = villagerType; + public void setAdult(Boolean adult) { + this.adult = adult; + } + + /** + * @param chest the chest to set + */ + public void setChest(Boolean chest) { + this.chest = chest; + } + + /** + * @param color the color to set + */ + public void setColor(DyeColor color) { + this.color = color; + } + + /** + * @param customName the customName to set + */ + public void setCustomName(String customName) { + this.customName = customName; + } + + /** + * Sets any display entity properties to the location, e.g. holograms + * @param pos location + */ + public void setDisplay(Location pos) { + World world = pos.getWorld(); + Location newPos = pos.clone().add(new Vector(x - 0.5D, y, z - 0.5D)); + Display d = null; + if (this.blockDisp != null) { + // Block Display + d = world.spawn(newPos, BlockDisplay.class); + BlockData bd = Bukkit.createBlockData(this.blockDisp.getBlockData()); + ((BlockDisplay) d).setBlock(bd); + } else if (this.itemDisp != null) { + // Item Display + d = world.spawn(newPos, ItemDisplay.class); + ((ItemDisplay) d).setItemStack(itemDisp.item()); + ((ItemDisplay) d).setItemDisplayTransform(itemDisp.itemDispTrans()); + } else if (this.textDisp != null) { + // Text Display + d = world.spawn(newPos, TextDisplay.class); + ((TextDisplay) d).setText(textDisp.text()); + ((TextDisplay) d).setAlignment(textDisp.alignment()); + ((TextDisplay) d).setBackgroundColor(textDisp.bgColor()); + ((TextDisplay) d).setLineWidth(textDisp.lWidth()); + ((TextDisplay) d).setTextOpacity(textDisp.opacity()); + ((TextDisplay) d).setShadowed(textDisp.isShadowed()); + ((TextDisplay) d).setSeeThrough(textDisp.isSeeThrough()); + ((TextDisplay) d).setBackgroundColor(textDisp.bgColor()); + } + if (d != null && this.displayRec != null) { + d.setCustomName(getCustomName()); + d.setBillboard(displayRec.billboard()); + d.setBrightness(displayRec.brightness()); + d.setDisplayHeight(displayRec.height()); + d.setDisplayWidth(displayRec.width()); + d.setGlowColorOverride(displayRec.glowColorOverride()); + d.setInterpolationDelay(displayRec.interpolationDelay()); + d.setInterpolationDuration(displayRec.interpolationDuration()); + d.setShadowRadius(displayRec.shadowRadius()); + d.setShadowStrength(displayRec.shadowStrength()); + d.setTeleportDuration(displayRec.teleportDuration()); + d.setTransformation(displayRec.transformation()); + d.setViewRange(displayRec.range()); + + // Spawn an armor stand here so that we have a way to detect if a player interacts with the item + ArmorStand armorStand = (ArmorStand) world.spawnEntity(newPos, EntityType.ARMOR_STAND); + armorStand.setSmall(true); // Reduces size + armorStand.setGravity(false); // Prevents falling + armorStand.setInvisible(true); + NamespacedKey key = new NamespacedKey(BentoBox.getInstance(), "associatedDisplayEntity"); + armorStand.getPersistentDataContainer().set(key, PersistentDataType.STRING, d.getUniqueId().toString()); + } + } + + /** + * @param domestication the domestication to set + */ + public void setDomestication(int domestication) { + this.domestication = domestication; } /** @@ -288,13 +456,24 @@ public class BlueprintEntity { } /** - * @return the mythicMobsRecord + * @param experience the experience to set */ - public MythicMobRecord getMythicMobsRecord() { - if (this.MMtype == null || this.MMLevel == null || this.MMpower == null || this.MMStance == null) { - return null; - } - return new MythicMobRecord(this.MMtype, this.getCustomName(), this.MMLevel, this.MMpower, this.MMStance); + public void setExperience(Integer experience) { + this.experience = experience; + } + + /** + * @param inventory the inventory to set + */ + public void setInventory(Map inventory) { + this.inventory = inventory; + } + + /** + * @param level the level to set + */ + public void setLevel(Integer level) { + this.level = level; } /** @@ -308,38 +487,164 @@ public class BlueprintEntity { this.MMStance = mmr.stance(); this.MMpower = mmr.power(); } - /** - * @return the npc + * @param npc the citizen to set */ - public String getNpc() { - return npc; + public void setNpc(String npc) { + this.npc = npc; + } + /** + * @param profession the profession to set + */ + public void setProfession(Profession profession) { + this.profession = profession; + } + /** + * @param style the style to set + */ + public void setStyle(Style style) { + this.style = style; } /** - * @param citizen the citizen to set + * @param tamed the tamed to set */ - public void setNpc(String citizen) { - this.npc = citizen; + public void setTamed(Boolean tamed) { + this.tamed = tamed; } - @Override - public String toString() { - return "BlueprintEntity [" + (npc != null ? "npc=" + npc + ", " : "") - + (MMtype != null ? "MMtype=" + MMtype + ", " : "") - + (MMLevel != null ? "MMLevel=" + MMLevel + ", " : "") - + (MMStance != null ? "MMStance=" + MMStance + ", " : "") - + (MMpower != null ? "MMpower=" + MMpower + ", " : "") + (color != null ? "color=" + color + ", " : "") - + (type != null ? "type=" + type + ", " : "") - + (customName != null ? "customName=" + customName + ", " : "") - + (tamed != null ? "tamed=" + tamed + ", " : "") + (chest != null ? "chest=" + chest + ", " : "") - + (adult != null ? "adult=" + adult + ", " : "") - + (domestication != null ? "domestication=" + domestication + ", " : "") - + (inventory != null ? "inventory=" + inventory + ", " : "") - + (style != null ? "style=" + style + ", " : "") + (level != null ? "level=" + level + ", " : "") - + (profession != null ? "profession=" + profession + ", " : "") - + (experience != null ? "experience=" + experience + ", " : "") - + (villagerType != null ? "villagerType=" + villagerType : "") + "]"; + /** + * @param type the type to set + */ + public void setType(EntityType type) { + this.type = type; + } + + /** + * @param v - villager + * @since 1.16.0 + */ + private void setVillager(Villager v) { + v.setProfession(profession == null ? Profession.NONE : profession); + v.setVillagerExperience(experience == null ? 0 : experience); + v.setVillagerLevel(level == null ? 0 : level); + v.setVillagerType(villagerType == null ? Villager.Type.PLAINS : villagerType); + } + + /** + * @param villagerType the villagerType to set + */ + public void setVillagerType(Villager.Type villagerType) { + this.villagerType = villagerType; + } + + /** + * BlockDisplay, ItemDisplay, TextDisplay + * @param disp display entity + */ + public void storeDisplay(Display disp) { + // Generic items + displayRec = new DisplayRec(disp.getBillboard(), disp.getBrightness(), disp.getDisplayHeight(), + disp.getDisplayWidth(), disp.getGlowColorOverride(), disp.getInterpolationDelay(), + disp.getInterpolationDuration(), disp.getShadowRadius(), disp.getShadowStrength(), + disp.getTeleportDuration(), disp.getTransformation(), disp.getViewRange()); + // Class specific items + if (disp instanceof BlockDisplay bd) { + this.blockDisp = new BlueprintBlock(bd.getBlock().getAsString()); + } else if (disp instanceof ItemDisplay id) { + itemDisp = new ItemDispRec(id.getItemStack(), id.getItemDisplayTransform()); + } else if (disp instanceof TextDisplay td) { + textDisp = new TextDisplayRec(td.getText(), td.getAlignment(), td.getBackgroundColor(), + td.getFacing(), td.getLineWidth(), td.getTextOpacity(), td.isShadowed(), td.isSeeThrough(), + td.isDefaultBackground()); + } + // Store location within block + x = disp.getLocation().getX() - disp.getLocation().getBlockX(); + y = disp.getLocation().getY() - disp.getLocation().getBlockY(); + z = disp.getLocation().getZ() - disp.getLocation().getBlockZ(); + } + + /** + * @return the glowing + */ + public boolean isGlowing() { + return glowing; + } + + /** + * @param glowing the glowing to set + */ + public void setGlowing(boolean glowing) { + this.glowing = glowing; + } + + /** + * @return the gravity + */ + public boolean isGravity() { + return gravity; + } + + /** + * @param gravity the gravity to set + */ + public void setGravity(boolean gravity) { + this.gravity = gravity; + } + + /** + * @return the visualFire + */ + public boolean isVisualFire() { + return visualFire; + } + + /** + * @param visualFire the visualFire to set + */ + public void setVisualFire(boolean visualFire) { + this.visualFire = visualFire; + } + + /** + * @return the silent + */ + public boolean isSilent() { + return silent; + } + + /** + * @param silent the silent to set + */ + public void setSilent(boolean silent) { + this.silent = silent; + } + + /** + * @return the invulnerable + */ + public boolean isInvulnerable() { + return invulnerable; + } + + /** + * @param invulnerable the invulnerable to set + */ + public void setInvulnerable(boolean invulnerable) { + this.invulnerable = invulnerable; + } + + /** + * @return the fireTicks + */ + public int getFireTicks() { + return fireTicks; + } + + /** + * @param fireTicks the fireTicks to set + */ + public void setFireTicks(int fireTicks) { + this.fireTicks = fireTicks; } - } diff --git a/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java b/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java index e5efa70a9..13e5d3401 100644 --- a/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java +++ b/src/main/java/world/bentobox/bentobox/database/json/adapters/MaterialTypeAdapter.java @@ -17,7 +17,7 @@ import com.google.gson.stream.JsonWriter; /** * Minecraft 1.20 changed GRASS to SHORT_GRASS. This class provides and backwards compatibility when loading - * databased files stored with previous versions. It can be extended in the future if further enum changes are made. + * database files stored with previous versions. It can be extended in the future if further enum changes are made. * @author tastybento * @since 2.0.0 */ diff --git a/src/main/java/world/bentobox/bentobox/hooks/FancyNpcsHook.java b/src/main/java/world/bentobox/bentobox/hooks/FancyNpcsHook.java index acc5ba7d8..c8c375f12 100644 --- a/src/main/java/world/bentobox/bentobox/hooks/FancyNpcsHook.java +++ b/src/main/java/world/bentobox/bentobox/hooks/FancyNpcsHook.java @@ -10,6 +10,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -29,9 +30,10 @@ import de.oliver.fancynpcs.api.actions.ActionTrigger; import de.oliver.fancynpcs.api.actions.NpcAction; import de.oliver.fancynpcs.api.utils.NpcEquipmentSlot; import de.oliver.fancynpcs.api.utils.SkinFetcher; +import lol.pyr.znpcsplus.api.npc.NpcEntry; import net.kyori.adventure.text.format.NamedTextColor; import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.hooks.Hook; +import world.bentobox.bentobox.api.hooks.NPCHook; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; /** @@ -40,13 +42,13 @@ import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; * @author tastybento * @since 3.1.0 */ -public class FancyNpcsHook extends Hook { +public class FancyNpcsHook extends NPCHook { public FancyNpcsHook() { super("FancyNpcs", Material.PLAYER_HEAD); } - public String serializeNPC(Npc npc, Vector origin) { + String serializeNPC(Npc npc, Vector origin) { if (npc == null) { throw new IllegalArgumentException("NPC cannot be null."); } @@ -265,6 +267,26 @@ public class FancyNpcsHook extends Hook { return null; // The hook process shouldn't fail } + /** + * Return all NPCs in the chunk + * @param chunk chunk + * @return list of NPCs + */ + public List getNPCsInChunk(Chunk chunk) { + return FancyNpcsPlugin.get().getNpcManager().getAllNpcs().stream() + .filter(npc -> npc.getData().getLocation().getChunk().equals(chunk)).toList(); + } + + /** + * Remove all NPCs in chunk + * @param chunk chunk + */ + @Override + public void removeNPCsInChunk(Chunk chunk) { + getNPCsInChunk(chunk).forEach(npc -> npc.removeForAll()); + } + + @Override public Map> getNpcsInArea(World world, List vectorsToCopy, @Nullable Vector origin) { Map> bpEntities = new HashMap<>(); @@ -290,4 +312,5 @@ public class FancyNpcsHook extends Hook { } return bpEntities; } + } diff --git a/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java b/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java index d9a31dfef..9a538d7a5 100644 --- a/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java +++ b/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -17,7 +18,7 @@ import lol.pyr.znpcsplus.api.NpcApiProvider; import lol.pyr.znpcsplus.api.npc.NpcEntry; import lol.pyr.znpcsplus.util.NpcLocation; import world.bentobox.bentobox.BentoBox; -import world.bentobox.bentobox.api.hooks.Hook; +import world.bentobox.bentobox.api.hooks.NPCHook; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; import world.bentobox.bentobox.util.Util; @@ -27,7 +28,7 @@ import world.bentobox.bentobox.util.Util; * @author tastybento * @since 3.2.0 */ -public class ZNPCsPlusHook extends Hook { +public class ZNPCsPlusHook extends NPCHook { private static final String VERSION = "2.0.0-SNAPSHOT"; // Minimum version required @@ -35,13 +36,20 @@ public class ZNPCsPlusHook extends Hook { super("ZNPCsPlus", Material.PLAYER_HEAD); } - public String serializeNPC(NpcEntry entry, Vector origin) { + /** + * Serialize a NpcEntry + * @param entry NPC entry + * @param origin origin point of blueprint + * @return string serializing the NPC Entry + */ + String serializeNPC(NpcEntry entry, Vector origin) { String result = NpcApiProvider.get().getNpcSerializerRegistry().getSerializer(YamlConfiguration.class) .serialize(entry) .saveToString(); return result; } + @Override public boolean spawnNpc(String yaml, Location pos) throws InvalidConfigurationException { YamlConfiguration yaml2 = new YamlConfiguration(); yaml2.loadFromString(yaml); @@ -75,6 +83,7 @@ public class ZNPCsPlusHook extends Hook { + this.getPlugin().getDescription().getVersion(); } + @Override public Map> getNpcsInArea(World world, List vectorsToCopy, @Nullable Vector origin) { Map> bpEntities = new HashMap<>(); @@ -101,4 +110,23 @@ public class ZNPCsPlusHook extends Hook { } return bpEntities; } + + /** + * Get a list of all the NPC IDs in this chunk + * @param chunk chunk + * @return list of NPC IDs + */ + public List getNPCsInChunk(Chunk chunk) { + return NpcApiProvider.get().getNpcRegistry().getAll().stream() + .filter(npc -> npc.getNpc().getWorld().equals(chunk.getWorld())) // Only NPCs in this world + .filter(npc -> npc.getNpc().getLocation().toBukkitLocation(chunk.getWorld()).getChunk().equals(chunk)) // Only in this chunk + .map(npc -> npc.getId()) // IDs + .toList(); + } + + @Override + public void removeNPCsInChunk(Chunk chunk) { + getNPCsInChunk(chunk).forEach(NpcApiProvider.get().getNpcRegistry()::delete); + } + } diff --git a/src/main/java/world/bentobox/bentobox/managers/BlueprintsManager.java b/src/main/java/world/bentobox/bentobox/managers/BlueprintsManager.java index d13332ed5..ca6a6cef6 100644 --- a/src/main/java/world/bentobox/bentobox/managers/BlueprintsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/BlueprintsManager.java @@ -44,6 +44,7 @@ import world.bentobox.bentobox.api.metadata.MetaDataValue; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.blueprints.Blueprint; import world.bentobox.bentobox.blueprints.BlueprintPaster; +import world.bentobox.bentobox.blueprints.DisplayListener; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBundle; import world.bentobox.bentobox.database.json.BentoboxTypeAdapterFactory; @@ -113,6 +114,8 @@ public class BlueprintsManager { gson = builder.create(); // Loaded tracker blueprintsLoaded = new HashSet<>(); + // Register Display listeners + Bukkit.getPluginManager().registerEvents(new DisplayListener(), plugin); } /** diff --git a/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java b/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java index c3ac17183..4be154439 100644 --- a/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java +++ b/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java @@ -43,8 +43,10 @@ import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.hooks.Hook; import world.bentobox.bentobox.database.objects.IslandDeletion; +import world.bentobox.bentobox.hooks.FancyNpcsHook; import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.hooks.SlimefunHook; +import world.bentobox.bentobox.hooks.ZNPCsPlusHook; import world.bentobox.bentobox.util.MyBiomeGrid; /** @@ -56,9 +58,18 @@ import world.bentobox.bentobox.util.MyBiomeGrid; public abstract class CopyWorldRegenerator implements WorldRegenerator { private final BentoBox plugin; + private Optional npc; + private Optional znpc; protected CopyWorldRegenerator() { this.plugin = BentoBox.getInstance(); + // Fancy NPCs Hook + npc = plugin.getHooks().getHook("FancyNpcs").filter(FancyNpcsHook.class::isInstance) + .map(FancyNpcsHook.class::cast); + // ZNPCs Plus Hook + znpc = plugin.getHooks().getHook("ZNPCsPlus").filter(ZNPCsPlusHook.class::isInstance) + .map(ZNPCsPlusHook.class::cast); + } /** @@ -179,11 +190,20 @@ public abstract class CopyWorldRegenerator implements WorldRegenerator { ); // Similarly, when the chunk is loaded, remove all the entities in the chunk apart from players - CompletableFuture entitiesFuture = chunkFuture.thenAccept(chunk -> - // Remove all entities in chunk, including any dropped items as a result of clearing the blocks above - Arrays.stream(chunk.getEntities()) - .filter(e -> !(e instanceof Player) && di.inBounds(e.getLocation().getBlockX(), e.getLocation().getBlockZ())) - .forEach(Entity::remove)); + CompletableFuture entitiesFuture = chunkFuture.thenAccept(chunk -> { + // Remove all entities in chunk, including any dropped items as a result of clearing the blocks above + Arrays.stream(chunk.getEntities()) + .filter(e -> !(e instanceof Player) + && di.inBounds(e.getLocation().getBlockX(), e.getLocation().getBlockZ())) + .forEach(Entity::remove); + // Remove any NPCs + // Fancy NPCs Hook + npc.ifPresent(hook -> hook.removeNPCsInChunk(chunk)); + // ZNPCs Plus Hook + znpc.ifPresent(hook -> hook.removeNPCsInChunk(chunk)); + + }); + return CompletableFuture.allOf(invFuture, entitiesFuture); } @@ -310,6 +330,10 @@ public abstract class CopyWorldRegenerator implements WorldRegenerator { public CompletableFuture regenerateSimple(GameModeAddon gm, IslandDeletion di, World world) { CompletableFuture bigFuture = new CompletableFuture<>(); + if (world == null) { + bigFuture.complete(null); + return bigFuture; + } new BukkitRunnable() { private int chunkX = di.getMinXChunk(); private int chunkZ = di.getMinZChunk(); diff --git a/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java b/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java index 2e6083026..2de5ae600 100644 --- a/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java +++ b/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java @@ -189,6 +189,8 @@ public class DefaultPasteUtil { * @return true if Bukkit entity spawned, false another plugin entity spawned */ static boolean spawnBlueprintEntity(BlueprintEntity k, Location location, Island island) { + // Display Entity (holograms, etc.) + k.setDisplay(location); // FancyNpc entity if (k.getNpc() != null && plugin.getHooks().getHook("FancyNpcs").filter(mmh -> mmh instanceof FancyNpcsHook).map(mmh -> { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9ddec656a..d314626f3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -25,6 +25,8 @@ softdepend: - EconomyPlus - MythicMobs - ZNPCsPlus + - FancyNpcs + - FancyHolograms libraries: - mysql:mysql-connector-java:${mysql.version} diff --git a/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintCopyCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintCopyCommandTest.java index cdcbe9bea..af3d086cd 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintCopyCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintCopyCommandTest.java @@ -47,13 +47,15 @@ import world.bentobox.bentobox.managers.LocalesManager; @PrepareForTest({Bukkit.class, BentoBox.class, User.class }) public class AdminBlueprintCopyCommandTest { + @Mock + private BentoBox plugin; @Mock private AdminBlueprintCommand ac; @Mock private GameModeAddon addon; @Mock private User user; - @Mock + private BlueprintClipboard clip; private UUID uuid = UUID.randomUUID(); @Mock @@ -64,10 +66,12 @@ public class AdminBlueprintCopyCommandTest { */ @Before public void setUp() throws Exception { - // Set up plugin - BentoBox plugin = mock(BentoBox.class); + // Set up plugin // Set up plugin + // Required for NamespacedKey + when(plugin.getName()).thenReturn("BentoBox"); Whitebox.setInternalState(BentoBox.class, "instance", plugin); + clip = mock(BlueprintClipboard.class); // Blueprints Manager when(plugin.getBlueprintsManager()).thenReturn(bm); diff --git a/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintLoadCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintLoadCommandTest.java index 284028a36..64c5963bc 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintLoadCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintLoadCommandTest.java @@ -76,6 +76,8 @@ public class AdminBlueprintLoadCommandTest { @Before public void setUp() throws Exception { + // Required for NamespacedKey + when(plugin.getName()).thenReturn("BentoBox"); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); diff --git a/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintSaveCommandTest.java b/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintSaveCommandTest.java index ac5386a6c..a8d382fc4 100644 --- a/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintSaveCommandTest.java +++ b/src/test/java/world/bentobox/bentobox/api/commands/admin/blueprints/AdminBlueprintSaveCommandTest.java @@ -55,6 +55,8 @@ import world.bentobox.bentobox.mocks.ServerMocks; @PrepareForTest({Bukkit.class, BentoBox.class, User.class }) public class AdminBlueprintSaveCommandTest { + @Mock + private BentoBox plugin; private AdminBlueprintSaveCommand absc; @Mock private AdminBlueprintCommand ac; @@ -76,8 +78,8 @@ public class AdminBlueprintSaveCommandTest { @Before public void setUp() throws Exception { - // Set up plugin - BentoBox plugin = mock(BentoBox.class); + // Required for NamespacedKey + when(plugin.getName()).thenReturn("BentoBox"); Whitebox.setInternalState(BentoBox.class, "instance", plugin); // Hooks HooksManager hooksManager = mock(HooksManager.class); diff --git a/src/test/java/world/bentobox/bentobox/blueprints/BlueprintClipboardTest.java b/src/test/java/world/bentobox/bentobox/blueprints/BlueprintClipboardTest.java index 861b0f8be..b951aa4e8 100644 --- a/src/test/java/world/bentobox/bentobox/blueprints/BlueprintClipboardTest.java +++ b/src/test/java/world/bentobox/bentobox/blueprints/BlueprintClipboardTest.java @@ -58,6 +58,8 @@ public class BlueprintClipboardTest { */ @Before public void setUp() throws Exception { + // Required for NamespacedKey + when(plugin.getName()).thenReturn("BentoBox"); // Set up plugin Whitebox.setInternalState(BentoBox.class, "instance", plugin); // Hooks diff --git a/src/test/java/world/bentobox/bentobox/managers/BlueprintClipboardManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/BlueprintClipboardManagerTest.java index d6e4fc354..390257852 100644 --- a/src/test/java/world/bentobox/bentobox/managers/BlueprintClipboardManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/BlueprintClipboardManagerTest.java @@ -57,7 +57,7 @@ public class BlueprintClipboardManagerTest { @Mock private BentoBox plugin; - @Mock + private BlueprintClipboard clipboard; private File blueprintFolder; @@ -129,15 +129,19 @@ public class BlueprintClipboardManagerTest { */ @Before public void setUp() throws Exception { + // Set up plugin + // Required for NamespacedKey + when(plugin.getName()).thenReturn("BentoBox"); + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + + clipboard = mock(BlueprintClipboard.class); + server = ServerMocks.newServer(); PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); blueprintFolder = new File("blueprints"); // Clear any residual files tearDown(); - // Set up plugin - BentoBox plugin = mock(BentoBox.class); - Whitebox.setInternalState(BentoBox.class, "instance", plugin); // Hooks HooksManager hooksManager = mock(HooksManager.class); when(hooksManager.getHook(anyString())).thenReturn(Optional.empty());