diff --git a/.github/workflows/modrinth-publish.yml b/.github/workflows/modrinth-publish.yml index 8244fe239..9f8b505a8 100644 --- a/.github/workflows/modrinth-publish.yml +++ b/.github/workflows/modrinth-publish.yml @@ -21,8 +21,8 @@ jobs: cache: maven # This step will take the version tag from the release and replace it in `pom.xml` before building. - - name: Set version from release tag - run: mvn -B versions:set -DnewVersion=${{ github.event.release.tag_name }} -DgenerateBackupPoms=false + #- name: Set version from release tag + # run: mvn -B versions:set -DnewVersion=${{ github.event.release.tag_name }} -DgenerateBackupPoms=false - name: Build and package with Maven run: mvn -B clean package --file pom.xml diff --git a/pom.xml b/pom.xml index ffb166b74..1e46541fd 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 UTF-8 - 17 + 21 2.0.9 @@ -84,7 +84,7 @@ -LOCAL - 3.1.1 + 3.2.0 bentobox-world https://sonarcloud.io ${project.basedir}/lib @@ -161,12 +161,6 @@ papermc https://repo.papermc.io/repository/maven-public/ - - - maven-snapshots - https://repository.apache.org/content/repositories/snapshots/ - minecraft-repo https://libraries.minecraft.net/ @@ -198,6 +192,12 @@ FancyPlugins Repository https://repo.fancyplugins.de/releases + + + pyr-snapshots + Pyr's Repo + https://repo.pyr.lol/snapshots + @@ -233,6 +233,13 @@ 4.2.2 test + + + io.papermc.paper + paper-api + ${paper.version} + provided + org.spigotmc @@ -357,13 +364,6 @@ org.eclipse.jdt.annotation 2.2.600 - - - io.papermc - paperlib - 1.0.6 - compile - com.github.apachezy @@ -406,6 +406,20 @@ 2.4.0 provided + + + lol.pyr + znpcsplus-api + 2.0.0-SNAPSHOT + provided + + + + de.oliver + FancyHolograms + 2.4.1 + provided + @@ -454,7 +468,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.2 @@ -528,7 +542,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.4.0 + 3.6.0 true ${project.build.directory}/dependency-reduced-pom.xml diff --git a/src/main/java/world/bentobox/bentobox/BentoBox.java b/src/main/java/world/bentobox/bentobox/BentoBox.java index ff315d7de..1cb28844a 100644 --- a/src/main/java/world/bentobox/bentobox/BentoBox.java +++ b/src/main/java/world/bentobox/bentobox/BentoBox.java @@ -19,7 +19,6 @@ import world.bentobox.bentobox.api.configuration.Config; import world.bentobox.bentobox.api.events.BentoBoxReadyEvent; -import world.bentobox.bentobox.api.hooks.Hook; import world.bentobox.bentobox.api.localization.TextVariables; import world.bentobox.bentobox.api.user.Notifier; import world.bentobox.bentobox.api.user.User; @@ -33,6 +32,7 @@ import world.bentobox.bentobox.hooks.MythicMobsHook; import world.bentobox.bentobox.hooks.SlimefunHook; import world.bentobox.bentobox.hooks.VaultHook; +import world.bentobox.bentobox.hooks.ZNPCsPlusHook; import world.bentobox.bentobox.hooks.placeholders.PlaceholderAPIHook; import world.bentobox.bentobox.listeners.BannedCommands; import world.bentobox.bentobox.listeners.BlockEndDragon; @@ -196,6 +196,8 @@ private void completeSetup(long loadTime) { // FancyNpcs hooksManager.registerHook(new FancyNpcsHook()); + // ZNPCsPlus + hooksManager.registerHook(new ZNPCsPlusHook()); // MythicMobs hooksManager.registerHook(new MythicMobsHook()); 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..fdcbca181 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/hooks/NPCHook.java @@ -0,0 +1,39 @@ +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 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 dc10da175..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 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; @@ -45,6 +40,7 @@ import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; import world.bentobox.bentobox.hooks.FancyNpcsHook; import world.bentobox.bentobox.hooks.MythicMobsHook; +import world.bentobox.bentobox.hooks.ZNPCsPlusHook; /** * The clipboard provides the holding spot for an active blueprint that is being @@ -56,6 +52,10 @@ */ 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; @@ -71,6 +71,8 @@ public class BlueprintClipboard { private final BentoBox plugin = BentoBox.getInstance(); private Optional mmh; private Optional npc; + private Optional znpc; + /** * Create a clipboard for blueprint @@ -82,12 +84,15 @@ public BlueprintClipboard(@NonNull Blueprint blueprint) { } public BlueprintClipboard() { - // Citizens Hook + // Fancy NPCs Hook npc = plugin.getHooks().getHook("FancyNpcs").filter(FancyNpcsHook.class::isInstance) .map(FancyNpcsHook.class::cast); // MythicMobs Hook mmh = plugin.getHooks().getHook("MythicMobs").filter(MythicMobsHook.class::isInstance) .map(MythicMobsHook.class::cast); + // ZNPCs Plus Hook + znpc = plugin.getHooks().getHook("ZNPCsPlus").filter(ZNPCsPlusHook.class::isInstance) + .map(ZNPCsPlusHook.class::cast); } /** @@ -143,6 +148,10 @@ private void copyAsync(World world, User user, List vectorsToCopy, int s // 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)); + } // Repeating copy task copyTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { @@ -154,9 +163,9 @@ private void copyAsync(World world, User user, List vectorsToCopy, int s 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++; @@ -222,7 +231,6 @@ private boolean copyBlock(Location l, boolean copyAir, boolean copyBiome, List()); @@ -276,11 +283,9 @@ private BlueprintBlock bluePrintBlock(Vector pos, Block block, boolean copyBiome } } } - if (blockState instanceof CreatureSpawner spawner) { b.setCreatureSpawner(getSpawner(spawner)); } - // Banners if (blockState instanceof Banner banner) { b.setBannerPatterns(banner.getPatterns()); @@ -309,62 +314,15 @@ private BlueprintCreatureSpawner getSpawner(CreatureSpawner spawner) { 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/BlueprintPaster.java b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java index 8ba1cb498..06a53e40a 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java @@ -240,7 +240,7 @@ private void pasteEntities(Bits bits, int count, Optional owner, int paste int x = location.getBlockX() + entry.getKey().getBlockX(); int y = location.getBlockY() + entry.getKey().getBlockY(); int z = location.getBlockZ() + entry.getKey().getBlockZ(); - Location center = new Location(world, x, y, z).add(new Vector(0.5, 0.5, 0.5)); + Location center = new Location(world, x, y, z).add(new Vector(0.5, 0D, 0.5)); List entities = entry.getValue(); entityMap.put(center, entities); count++; 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; + /** + * Item Display Entity store + * @since 3.2.0 + */ + public record ItemDispRec(@Expose ItemStack item, @Expose ItemDisplayTransform itemDispTrans) {} + + /** + * 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 - String MMStance; + private Boolean adult; @Expose - Float MMpower; + public BlueprintBlock blockDisp; @Expose - private DyeColor color; + private Boolean chest; @Expose - private EntityType type; + private DyeColor color; @Expose private String customName; @Expose - private Boolean tamed; - @Expose - private Boolean chest; - @Expose - private Boolean adult; + public DisplayRec displayRec; @Expose private Integer domestication; @Expose + private Integer experience; + @Expose private Map inventory; @Expose - private Style style; + 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 Integer experience; + 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,19 +258,21 @@ public void configureEntity(Entity e) { if (style != null && e instanceof Horse horse) { horse.setStyle(style); } + // 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)); } - /** - * @param v - villager - * @since 1.16.0 + * @return the adult */ - 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); + public Boolean getAdult() { + return adult; + } + /** + * @return the chest + */ + public Boolean getChest() { + return chest; } - /** * @return the color */ @@ -122,100 +280,224 @@ public DyeColor getColor() { return color; } /** - * @param color the color to set + * @return the customName */ - public void setColor(DyeColor color) { - this.color = color; + public String getCustomName() { + return customName; } /** - * @return the type + * @return the domestication */ - public EntityType getType() { - return type; + public Integer getDomestication() { + return domestication; } /** - * @param type the type to set + * @return the experience */ - public void setType(EntityType type) { - this.type = type; + public Integer getExperience() { + return experience; } /** - * @return the customName + * @return the inventory */ - public String getCustomName() { - return customName; + public Map getInventory() { + return inventory; } /** - * @param customName the customName to set + * @return the level */ - public void setCustomName(String customName) { - this.customName = customName; + 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 + */ + public Style getStyle() { + return style; } + /** * @return the tamed */ public Boolean getTamed() { return tamed; } + /** - * @param tamed the tamed to set + * @return the type */ - public void setTamed(Boolean tamed) { - this.tamed = tamed; + public EntityType getType() { + return type; } + /** - * @return the chest + * @return the villagerType */ - public Boolean getChest() { - return chest; + public Villager.Type getVillagerType() { + return villagerType; + } + + /** + * @param adult the adult to set + */ + public void setAdult(Boolean adult) { + this.adult = adult; } + /** * @param chest the chest to set */ public void setChest(Boolean chest) { this.chest = chest; } + /** - * @return the adult + * @param color the color to set */ - public Boolean getAdult() { - return adult; + public void setColor(DyeColor color) { + this.color = color; } + /** - * @param adult the adult to set + * @param customName the customName to set */ - public void setAdult(Boolean adult) { - this.adult = adult; + public void setCustomName(String customName) { + this.customName = customName; } + /** - * @return the domestication - */ - public Integer getDomestication() { - return domestication; + * 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; } + /** - * @return the inventory + * @param domestication the domestication to set */ - public Map getInventory() { - return inventory; + public void setDomestication(Integer domestication) { + this.domestication = domestication; + } + + /** + * @param experience the experience to set + */ + public void setExperience(Integer experience) { + this.experience = experience; } + /** * @param inventory the inventory to set */ public void setInventory(Map inventory) { this.inventory = inventory; } + /** - * @return the style + * @param level the level to set */ - public Style getStyle() { - return style; + public void setLevel(Integer level) { + this.level = level; + } + + /** + * @param mmr the mythicMobsRecord to set + * @since 2.1.0 + */ + public void setMythicMobsRecord(MythicMobRecord mmr) { + this.setCustomName(mmr.displayName()); + this.MMtype = mmr.type(); + this.MMLevel = mmr.level(); + this.MMStance = mmr.stance(); + this.MMpower = mmr.power(); + } + /** + * @param npc the citizen to set + */ + 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 @@ -225,121 +507,144 @@ public void setStyle(Style style) { } /** - * @return the level + * @param tamed the tamed to set */ - public Integer getLevel() { - return level; + public void setTamed(Boolean tamed) { + this.tamed = tamed; } /** - * @param level the level to set + * @param type the type to set */ - public void setLevel(Integer level) { - this.level = level; + public void setType(EntityType type) { + this.type = type; } /** - * @return the profession + * @param v - villager + * @since 1.16.0 */ - public Profession getProfession() { - return profession; + 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 profession the profession to set + * @param villagerType the villagerType to set */ - public void setProfession(Profession profession) { - this.profession = profession; + public void setVillagerType(Villager.Type villagerType) { + this.villagerType = villagerType; } /** - * @return the experience + * 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 Integer getExperience() { - return experience; + public boolean isGlowing() { + return glowing; } /** - * @param experience the experience to set + * @param glowing the glowing to set */ - public void setExperience(Integer experience) { - this.experience = experience; + public void setGlowing(boolean glowing) { + this.glowing = glowing; } /** - * @return the villagerType + * @return the gravity */ - public Villager.Type getVillagerType() { - return villagerType; + public boolean isGravity() { + return gravity; } /** - * @param villagerType the villagerType to set + * @param gravity the gravity to set */ - public void setVillagerType(Villager.Type villagerType) { - this.villagerType = villagerType; + public void setGravity(boolean gravity) { + this.gravity = gravity; } /** - * @param domestication the domestication to set + * @return the visualFire */ - public void setDomestication(Integer domestication) { - this.domestication = domestication; + public boolean isVisualFire() { + return visualFire; } /** - * @return the mythicMobsRecord + * @param visualFire the visualFire 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 setVisualFire(boolean visualFire) { + this.visualFire = visualFire; } /** - * @param mmr the mythicMobsRecord to set - * @since 2.1.0 + * @return the silent */ - public void setMythicMobsRecord(MythicMobRecord mmr) { - this.setCustomName(mmr.displayName()); - this.MMtype = mmr.type(); - this.MMLevel = mmr.level(); - this.MMStance = mmr.stance(); - this.MMpower = mmr.power(); + public boolean isSilent() { + return silent; } /** - * @return the npc + * @param silent the silent to set */ - public String getNpc() { - return npc; + public void setSilent(boolean silent) { + this.silent = silent; } /** - * @param citizen the citizen to set + * @return the invulnerable */ - public void setNpc(String citizen) { - this.npc = citizen; + public boolean isInvulnerable() { + return invulnerable; } - @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 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/Database.java b/src/main/java/world/bentobox/bentobox/database/Database.java index eca983ac2..8b0c4aea3 100644 --- a/src/main/java/world/bentobox/bentobox/database/Database.java +++ b/src/main/java/world/bentobox/bentobox/database/Database.java @@ -167,8 +167,8 @@ public static Set> getDataobjects() { } /** - * Load all objects async - * @return CompletableFuture> + * Load all objects asynchronously. + * @return {@code CompletableFuture>} */ public @NonNull CompletableFuture> loadObjectsASync() { return handler.loadObjectsASync(); 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 @@ /** * 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..528289e24 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.concurrent.ConcurrentHashMap; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -31,7 +32,7 @@ import de.oliver.fancynpcs.api.utils.SkinFetcher; 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 +41,13 @@ * @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 +266,26 @@ public String getFailureCause() { 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 +311,5 @@ public String getFailureCause() { } return bpEntities; } + } diff --git a/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java b/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java new file mode 100644 index 000000000..9a538d7a5 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/hooks/ZNPCsPlusHook.java @@ -0,0 +1,132 @@ +package world.bentobox.bentobox.hooks; + +import java.util.ArrayList; +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; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.Nullable; + +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.NPCHook; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; +import world.bentobox.bentobox.util.Util; + +/** + * Provides copy and pasting of ZNPCS Plus in blueprints https://github.com/Pyrbu/ZNPCsPlus + * + * @author tastybento + * @since 3.2.0 + */ +public class ZNPCsPlusHook extends NPCHook { + + private static final String VERSION = "2.0.0-SNAPSHOT"; // Minimum version required + + public ZNPCsPlusHook() { + super("ZNPCsPlus", Material.PLAYER_HEAD); + } + + /** + * 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); + NpcEntry entry = NpcApiProvider.get().getNpcSerializerRegistry().getSerializer(YamlConfiguration.class) + .deserialize(yaml2); + NpcLocation loc = new NpcLocation(pos); + entry.getNpc().setLocation(loc); + NpcApiProvider.get().getNpcRegistry().register(entry); + + return true; + } + + @Override + public boolean hook() { + boolean hooked = this.isPluginAvailable(); + // Check version + String version = this.getPlugin().getDescription().getVersion(); + if (!Util.isVersionCompatible(version, VERSION)) { + return false; + } + if (!hooked) { + BentoBox.getInstance().logError("Could not hook into FancyNpcs"); + } + return hooked; + } + + @Override + public String getFailureCause() { + // The only failure is wrong version + return "ZNPCsPlus version " + VERSION + " required or later. You are running " + + this.getPlugin().getDescription().getVersion(); + } + + @Override + public Map> getNpcsInArea(World world, List vectorsToCopy, + @Nullable Vector origin) { + Map> bpEntities = new HashMap<>(); + + for (NpcEntry npcEntry : NpcApiProvider.get().getNpcRegistry().getAll()) { + NpcLocation npcLocation = npcEntry.getNpc().getLocation(); + Vector loc = new Vector(npcLocation.getBlockX(), npcLocation.getBlockY(), npcLocation.getBlockZ()); + if (npcEntry.getNpc().getWorld().equals(world) && vectorsToCopy.contains(loc)) { + // Put the NPC into a BlueprintEntity - serialize it + BlueprintEntity cit = new BlueprintEntity(); + cit.setNpc(this.serializeNPC(npcEntry, origin)); + // Retrieve or create the list of entities and add this one + List entities = bpEntities.getOrDefault(loc, new ArrayList<>()); + entities.add(cit); + // Create the position where this entity will be pasted relative to the location + Vector origin2 = origin == null ? new Vector(0, 0, 0) : origin; + int x = loc.getBlockX() - origin2.getBlockX(); + int y = loc.getBlockY() - origin2.getBlockY(); + int z = loc.getBlockZ() - origin2.getBlockZ(); + Vector pos = new Vector(x, y, z); + // Store + bpEntities.put(pos, entities); // Update the map + } + } + 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/listeners/flags/protection/LockAndBanListener.java b/src/main/java/world/bentobox/bentobox/listeners/flags/protection/LockAndBanListener.java index 6a41cb2ca..9295eb8a7 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/protection/LockAndBanListener.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/protection/LockAndBanListener.java @@ -13,11 +13,11 @@ import org.bukkit.util.Vector; import org.eclipse.jdt.annotation.NonNull; -import io.papermc.lib.PaperLib; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.flags.FlagListener; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.lists.Flags; +import world.bentobox.bentobox.util.Util; /** * Listener for the lock flag @@ -164,7 +164,7 @@ private void eject(Player player) { // We'll try to teleport him to the spawn... Location l = player.getWorld().getSpawnLocation(); if (l != null) { - PaperLib.teleportAsync(player, l); + Util.teleportAsync(player, l); } // Switch him back to the default gamemode. He may die, sorry :( 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.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 BlueprintsManager(@NonNull BentoBox plugin) { 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/managers/IslandsManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java index 53c25b4ba..30037e739 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandsManager.java @@ -40,7 +40,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import io.papermc.lib.PaperLib; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.events.IslandBaseEvent; import world.bentobox.bentobox.api.events.island.IslandEvent; @@ -1080,7 +1079,7 @@ private CompletableFuture homeTeleportAsync(@NonNull World world, @NonN .ifFail(() -> goingHome.remove(user.getUniqueId())).buildFuture().thenAccept(result::complete); return; } - PaperLib.teleportAsync(Objects.requireNonNull(player), home).thenAccept(b -> { + Util.teleportAsync(Objects.requireNonNull(player), home).thenAccept(b -> { // Only run the commands if the player is successfully teleported if (Boolean.TRUE.equals(b)) { teleported(world, user, name, newIsland, island); @@ -1192,7 +1191,6 @@ public void spawnTeleport(@NonNull World world, @NonNull Player player) { * * @param player player */ - @SuppressWarnings("deprecation") private void readyPlayer(@NonNull Player player) { // Stop any gliding player.setGliding(false); @@ -1469,7 +1467,7 @@ public void removePlayersFromIsland(Island island) { } else { // Move player to spawn getSpawn(w).map(i -> i.getSpawnPoint(w.getEnvironment())).filter(Objects::nonNull) - .ifPresentOrElse(sp -> PaperLib.teleportAsync(p, sp), + .ifPresentOrElse(sp -> Util.teleportAsync(p, sp), () -> plugin.logWarning("Spawn exists but its location is null!")); } diff --git a/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java b/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java index 198e8950c..59aec708d 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/NewIsland.java @@ -15,9 +15,9 @@ import world.bentobox.bentobox.api.events.island.IslandCreateEvent; import world.bentobox.bentobox.api.events.island.IslandEvent; import world.bentobox.bentobox.api.events.island.IslandEvent.Reason; +import world.bentobox.bentobox.api.events.island.IslandResetEvent; import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.api.logs.LogEntry.LogType; -import world.bentobox.bentobox.api.events.island.IslandResetEvent; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.BlueprintsManager; diff --git a/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java b/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java index c3ac17183..dec80a8e3 100644 --- a/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java +++ b/src/main/java/world/bentobox/bentobox/nms/CopyWorldRegenerator.java @@ -38,14 +38,16 @@ import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import io.papermc.lib.PaperLib; 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; +import world.bentobox.bentobox.util.Util; /** * Regenerates by using a seed world. The seed world is created using the same generator as the game @@ -56,9 +58,18 @@ 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); + } /** @@ -137,7 +148,7 @@ private CompletableFuture regenerateChunk(@Nullable IslandDeletion di, @No CompletableFuture seedWorldFuture = getSeedWorldChunk(world, chunkX, chunkZ); // Set up a future to get the chunk requests using Paper's Lib. If Paper is used, this should be done async - CompletableFuture chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ); + CompletableFuture chunkFuture = Util.getChunkAtAsync(world, chunkX, chunkZ); // If there is no island, do not clean chunk CompletableFuture cleanFuture = di != null ? cleanChunk(chunkFuture, di) : CompletableFuture.completedFuture(null); @@ -161,7 +172,7 @@ private CompletableFuture regenerateChunk(@Nullable IslandDeletion di, @No private CompletableFuture getSeedWorldChunk(World world, int chunkX, int chunkZ) { World seed = Bukkit.getWorld(world.getName() + "/bentobox"); if (seed == null) return CompletableFuture.completedFuture(null); - return PaperLib.getChunkAtAsync(seed, chunkX, chunkZ); + return Util.getChunkAtAsync(seed, chunkX, chunkZ); } /** @@ -179,11 +190,20 @@ private CompletableFuture cleanChunk(CompletableFuture chunkFuture, ); // 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); } @@ -227,6 +247,7 @@ private void copyChunkDataToChunk(Chunk toChunk, Chunk fromChunk, BoundingBox li Arrays.stream(fromChunk.getTileEntities()).forEach(bs -> processTileEntity(bs.getBlock(), bs.getLocation().toVector().toLocation(toChunk.getWorld()).getBlock())); } + @SuppressWarnings("deprecation") private void processEntity(Entity entity, Location location) { Entity bpe = location.getWorld().spawnEntity(location, entity.getType()); bpe.setCustomName(entity.getCustomName()); @@ -310,6 +331,10 @@ private void writeSign(Sign fromSign, Sign toSign, Side side) { 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(); @@ -350,7 +375,7 @@ private boolean isEnded(int chunkX) { @SuppressWarnings("deprecation") private CompletableFuture regenerateChunk(GameModeAddon gm, IslandDeletion di, @Nonnull World world, int chunkX, int chunkZ) { - CompletableFuture chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ); + CompletableFuture chunkFuture = Util.getChunkAtAsync(world, chunkX, chunkZ); CompletableFuture invFuture = chunkFuture.thenAccept(chunk -> Arrays.stream(chunk.getTileEntities()).filter(InventoryHolder.class::isInstance) .filter(te -> di.inBounds(te.getLocation().getBlockX(), te.getLocation().getBlockZ())) diff --git a/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java index c760a21d8..84e9898a3 100644 --- a/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java +++ b/src/main/java/world/bentobox/bentobox/panels/customizable/IslandHomesPanel.java @@ -60,7 +60,6 @@ public class IslandHomesPanel extends AbstractPanel * * @param command CompositeCommand * @param user User who opens panel - * @param islandMap map of island names and IslandInfo */ private IslandHomesPanel(@NonNull CompositeCommand command, @NonNull User user) { diff --git a/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java b/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java index 86ee2e070..2de5ae600 100644 --- a/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java +++ b/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java @@ -36,6 +36,7 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.hooks.FancyNpcsHook; import world.bentobox.bentobox.hooks.MythicMobsHook; +import world.bentobox.bentobox.hooks.ZNPCsPlusHook; import world.bentobox.bentobox.nms.PasteHandler; /** @@ -176,8 +177,8 @@ public static void setSpawner(CreatureSpawner spawner, BlueprintCreatureSpawner public static CompletableFuture setEntity(Island island, Location location, List list) { World world = location.getWorld(); assert world != null; - return Util.getChunkAtAsync(location).thenRun(() -> list.stream().filter(k -> k.getType() != null) - .forEach(k -> spawnBlueprintEntity(k, location, island))); + return Util.getChunkAtAsync(location) + .thenRun(() -> list.stream().forEach(k -> spawnBlueprintEntity(k, location, island))); } /** @@ -188,7 +189,9 @@ public static CompletableFuture setEntity(Island island, Location location * @return true if Bukkit entity spawned, false another plugin entity spawned */ static boolean spawnBlueprintEntity(BlueprintEntity k, Location location, Island island) { - // Npc entity + // Display Entity (holograms, etc.) + k.setDisplay(location); + // FancyNpc entity if (k.getNpc() != null && plugin.getHooks().getHook("FancyNpcs").filter(mmh -> mmh instanceof FancyNpcsHook).map(mmh -> { try { @@ -201,6 +204,19 @@ static boolean spawnBlueprintEntity(BlueprintEntity k, Location location, Island // Npc has spawned. return false; } + // ZNPCsPlus + if (k.getNpc() != null + && plugin.getHooks().getHook("ZNPCsPlus").filter(mmh -> mmh instanceof ZNPCsPlusHook).map(znpch -> { + try { + return ((ZNPCsPlusHook) znpch).spawnNpc(k.getNpc(), location); + } catch (InvalidConfigurationException e) { + plugin.logError("ZNPCsPlus loading failed in blueprint."); + return false; + } + }).orElse(false)) { + // Npc has spawned. + return false; + } // Mythic Mobs entity if (k.getMythicMobsRecord() != null && plugin.getHooks().getHook("MythicMobs") @@ -210,6 +226,10 @@ static boolean spawnBlueprintEntity(BlueprintEntity k, Location location, Island // MythicMob has spawned. return false; } + if (k.getType() == null) { + // Nothing + return false; + } LivingEntity e = (LivingEntity) location.getWorld().spawnEntity(location, k.getType()); if (k.getCustomName() != null) { String customName = k.getCustomName(); diff --git a/src/main/java/world/bentobox/bentobox/util/Util.java b/src/main/java/world/bentobox/bentobox/util/Util.java index 00434d260..4aa1ccc1f 100644 --- a/src/main/java/world/bentobox/bentobox/util/Util.java +++ b/src/main/java/world/bentobox/bentobox/util/Util.java @@ -1,6 +1,7 @@ package world.bentobox.bentobox.util; import java.io.IOException; +import java.lang.reflect.Method; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -26,7 +27,6 @@ import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.attribute.Attribute; -import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.Allay; import org.bukkit.entity.Animals; @@ -51,8 +51,6 @@ import com.google.common.base.Enums; import com.google.common.base.Optional; -import io.papermc.lib.PaperLib; -import io.papermc.lib.features.blockstatesnapshot.BlockStateSnapshotResult; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.nms.PasteHandler; @@ -382,7 +380,7 @@ public static boolean isTamableEntity(Entity entity) { */ @NonNull public static CompletableFuture teleportAsync(@Nonnull Entity entity, @Nonnull Location location) { - return PaperLib.teleportAsync(entity, location); + return teleportAsync(entity, location, TeleportCause.UNKNOWN); } /** @@ -392,12 +390,26 @@ public static CompletableFuture teleportAsync(@Nonnull Entity entity, @ * @param cause The cause for the teleportation * @return Future that completes with the result of the teleport */ + @SuppressWarnings("unchecked") @NonNull public static CompletableFuture teleportAsync(@Nonnull Entity entity, @Nonnull Location location, TeleportCause cause) { - return PaperLib.teleportAsync(entity, location, cause); + try { + // Use reflection to check if the method exists + Method method = Entity.class.getMethod("teleportAsync", Location.class, TeleportCause.class); + if (method != null) { + // Invoke the method using reflection on the entity instance + return (CompletableFuture) method.invoke(entity, location, cause); + } + } catch (NoSuchMethodException e) { + // Method does not exist, fallback to Spigot behavior + } catch (Exception e) { + plugin.logStacktrace(e); // Report other exceptions + } + // Fallback for Spigot servers + entity.teleport(location, cause); + return CompletableFuture.completedFuture(true); } - /** * Gets the chunk at the target location, loading it asynchronously if needed. * @param loc Location to get chunk for @@ -440,9 +452,24 @@ public static CompletableFuture getChunkAtAsync(@Nonnull World world, int * @param gen Should the chunk generate or not. Only respected on some MC versions, 1.13 for CB, 1.12 for Paper * @return Future that completes with the chunk, or null if the chunk did not exists and generation was not requested. */ + @SuppressWarnings("unchecked") @NonNull public static CompletableFuture getChunkAtAsync(@Nonnull World world, int x, int z, boolean gen) { - return PaperLib.getChunkAtAsync(world, x, z, gen); + try { + // Use reflection to check if the method exists + Method method = World.class.getMethod("getChunkAtAsync", int.class, int.class, boolean.class); + if (method != null) { + // Invoke the method using reflection + return (CompletableFuture) method.invoke(world, x, z, gen); + } + } catch (NoSuchMethodException e) { + // Method does not exist, fallback to Spigot behavior + } catch (Exception e) { + e.printStackTrace(); // Handle other exceptions (optional) + } + // Fallback for Spigot servers + return CompletableFuture.completedFuture(world.getChunkAt(x, z, gen)); + } /** @@ -462,64 +489,55 @@ public static boolean isChunkGenerated(@NonNull Location loc) { * @return If the chunk is generated or not */ public static boolean isChunkGenerated(@Nonnull World world, int x, int z) { - return PaperLib.isChunkGenerated(world, x, z); - } - - /** - * Get's a BlockState, optionally not using a snapshot - * @param block The block to get a State of - * @param useSnapshot Whether or not to use a snapshot when supported - * @return The BlockState - */ - @NonNull - public static BlockStateSnapshotResult getBlockState(@Nonnull Block block, boolean useSnapshot) { - return PaperLib.getBlockState(block, useSnapshot); + return world.isChunkGenerated(x, z); } /** - * Detects if the current MC version is at least the following version. + * Checks if the given version is compatible with the required version. + * *

- * Assumes 0 patch version. - * - * @param minor Min Minor Version - * @return Meets the version requested - */ - public static boolean isVersion(int minor) { - return PaperLib.isVersion(minor); - } - - /** - * Detects if the current MC version is at least the following version. - * @param minor Min Minor Version - * @param patch Min Patch Version - * @return Meets the version requested - */ - public static boolean isVersion(int minor, int patch) { - return PaperLib.isVersion(minor, patch); - } - - /** - * Gets the current Minecraft Minor version. IE: 1.13.1 returns 13 - * @return The Minor Version - */ - public static int getMinecraftVersion() { - return PaperLib.getMinecraftVersion(); - } + * A version is considered compatible if: + *

    + *
  • The major, minor, and patch components of the given version are greater than or equal to those of the required version.
  • + *
  • If the numeric components are equal, the absence of "-SNAPSHOT" in the given version takes precedence (i.e., release versions are considered more compatible than SNAPSHOT versions).
  • + *
+ *

+ * + * @param version the version to check, in the format "major.minor.patch[-SNAPSHOT]". + * @param requiredVersion the required version, in the format "major.minor.patch[-SNAPSHOT]". + * @return {@code true} if the given version is compatible with the required version; {@code false} otherwise. + * + *

+ * Examples: + *

    + *
  • {@code isVersionCompatible("2.1.0", "2.0.0-SNAPSHOT")} returns {@code true}
  • + *
  • {@code isVersionCompatible("2.0.0", "2.0.0-SNAPSHOT")} returns {@code true}
  • + *
  • {@code isVersionCompatible("2.0.0-SNAPSHOT", "2.0.0")} returns {@code false}
  • + *
  • {@code isVersionCompatible("1.9.9", "2.0.0-SNAPSHOT")} returns {@code false}
  • + *
+ *

+ */ + public static boolean isVersionCompatible(String version, String requiredVersion) { + String[] versionParts = version.replace("-SNAPSHOT", "").split("\\."); + String[] requiredVersionParts = requiredVersion.replace("-SNAPSHOT", "").split("\\."); + + for (int i = 0; i < Math.max(versionParts.length, requiredVersionParts.length); i++) { + int vPart = i < versionParts.length ? Integer.parseInt(versionParts[i]) : 0; + int rPart = i < requiredVersionParts.length ? Integer.parseInt(requiredVersionParts[i]) : 0; + + if (vPart > rPart) { + return true; + } else if (vPart < rPart) { + return false; + } + } - /** - * Gets the current Minecraft Patch version. IE: 1.13.1 returns 1 - * @return The Patch Version - */ - public static int getMinecraftPatchVersion() { - return PaperLib.getMinecraftPatchVersion(); - } + // If numeric parts are equal, prioritize SNAPSHOT as lower precedence + boolean isVersionSnapshot = version.contains("-SNAPSHOT"); + boolean isRequiredSnapshot = requiredVersion.contains("-SNAPSHOT"); - /** - * Check if the server has access to the Spigot API - * @return True for Spigot and Paper environments - */ - public static boolean isSpigot() { - return PaperLib.isSpigot(); + // If required version is a full release but current version is SNAPSHOT, it's incompatible + return !(!isRequiredSnapshot && isVersionSnapshot); } /** @@ -527,29 +545,21 @@ public static boolean isSpigot() { * @return True for Paper environments */ public static boolean isPaper() { - return !isJUnitTest() && PaperLib.isPaper(); - } - - /** - * I don't like doing this, but otherwise we need to set a flag in every test - */ - private static boolean isJUnitTest() { - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - for (StackTraceElement element : stackTrace) { - if (element.getClassName().startsWith("org.junit.")) { - return true; - } + try { + Class.forName("com.destroystokyo.paper.PaperConfig"); + return true; // Paper-specific class exists + } catch (ClassNotFoundException e) { + return false; // Not a Paper server } - return false; } - /** * This method translates color codes in given string and strips whitespace after them. * This code parses both: hex and old color codes. * @param textToColor Text which color codes must be parsed. * @return String text with parsed colors and stripped whitespaces after them. */ + @SuppressWarnings("deprecation") @NonNull public static String translateColorCodes(@NonNull String textToColor) { // Use matcher to find hex patterns in given text. @@ -712,8 +722,14 @@ public static void runCommands(User user, String ownerName, @NonNull List ser; + @Mock + private NpcRegistry registry; + @Mock + private Npc npc; + @Mock + private NpcLocation npcLoc; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + // Set up plugin + plugin = mock(BentoBox.class); + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + // Bukkit + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + when(Bukkit.getPluginManager()).thenReturn(pim); + when(npcPlugin.getDescription()).thenReturn(new PluginDescriptionFile("ZNPCsPlus", "2.0.0-SNAPSHOT", "main")); + when(pim.getPlugin("ZNPCsPlus")).thenReturn(npcPlugin); + // Location + when(world.getName()).thenReturn("bskyblock"); + when(location.getWorld()).thenReturn(world); + // NpcApiProvider + PowerMockito.mockStatic(NpcApiProvider.class, Mockito.RETURNS_MOCKS); + when(NpcApiProvider.get()).thenReturn(npcApi); + + when(registry.getAll()).thenAnswer(invocation -> List.of(entry)); + + when(npcLoc.getBlockX()).thenReturn(0); + when(npcLoc.getBlockY()).thenReturn(0); + when(npcLoc.getBlockZ()).thenReturn(0); + when(npc.getWorld()).thenReturn(world); + + when(npc.getLocation()).thenReturn(npcLoc); + + when(npcApi.getNpcRegistry()).thenReturn(registry); + when(npcApi.getNpcSerializerRegistry()).thenReturn(npcSerReg); + when(npcSerReg.getSerializer(any())).thenReturn(ser); + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("test", "test"); + when(ser.serialize(any())).thenReturn(yaml); + when(entry.getNpc()).thenReturn(npc); + when(ser.deserialize(any())).thenReturn(entry); + + + hook = new ZNPCsPlusHook(); + } + + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ZNPCsPlusHook#hook()}. + */ + @Test + public void testHook() { + // Not hooked + assertFalse(hook.hook()); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ZNPCsPlusHook#getFailureCause()}. + */ + @Test + public void testGetFailureCause() { + when(npcPlugin.getDescription()).thenReturn(new PluginDescriptionFile("ZNPCsPlus", "1.0.0", "main")); + assertEquals("ZNPCsPlus version 2.0.0-SNAPSHOT required or later. You are running 1.0.0", + hook.getFailureCause()); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ZNPCsPlusHook#ZNPCsPlusHook()}. + */ + @Test + public void testZNPCsPlusHook() { + assertNotNull(hook); + assertEquals(Material.PLAYER_HEAD, hook.getIcon()); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ZNPCsPlusHook#serializeNPC(lol.pyr.znpcsplus.api.npc.NpcEntry, org.bukkit.util.Vector)}. + */ + @Test + public void testSerializeNPC() { + assertEquals("test: test\n", hook.serializeNPC(entry, new Vector(1, 1, 1))); + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ZNPCsPlusHook#spawnNpc(java.lang.String, org.bukkit.Location)}. + */ + @Test + public void testSpawnNpc() { + try { + assertTrue(hook.spawnNpc("", location)); + } catch (InvalidConfigurationException e) { + e.printStackTrace(); + } + } + + /** + * Test method for {@link world.bentobox.bentobox.hooks.ZNPCsPlusHook#getNpcsInArea(org.bukkit.World, java.util.List, org.bukkit.util.Vector)}. + */ + @Test + public void testGetNpcsInArea() { + hook.getNpcsInArea(world, List.of(new Vector(0, 0, 0)), new Vector(0, 0, 0)); + } + +} diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BreakBlocksListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BreakBlocksListenerTest.java index 6a14ef448..c85479552 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BreakBlocksListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/protection/BreakBlocksListenerTest.java @@ -338,7 +338,7 @@ public void testOnVehicleDamageEventNotAllowedBoat() { when(island.isAllowed(any(), any())).thenReturn(false); Vehicle vehicle = mock(Vehicle.class); when(vehicle.getLocation()).thenReturn(location); - when(vehicle.getType()).thenReturn(EntityType.OAK_BOAT); + when(vehicle.getType()).thenReturn(EntityType.BOAT); VehicleDamageEvent e = new VehicleDamageEvent(vehicle, mockPlayer, 10); bbl.onVehicleDamageEvent(e); assertTrue(e.isCancelled()); diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/InvincibleVisitorsListenerTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/InvincibleVisitorsListenerTest.java index fc7181d3f..a4a1139b3 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/InvincibleVisitorsListenerTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/worldsettings/InvincibleVisitorsListenerTest.java @@ -181,7 +181,7 @@ public void setUp() throws Exception { when(top.getSize()).thenReturn(9); when(panel.getInventory()).thenReturn(top); - when(Bukkit.createInventory(any(), anyInt(), any())).thenReturn(top); + when(Bukkit.createInventory(any(), anyInt(), anyString())).thenReturn(top); } @After 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 @@ private void zip(File targetFile) throws IOException { */ @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()); diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java index c64cbf844..74f6cc514 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandsManagerTest.java @@ -74,9 +74,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; -import io.papermc.lib.PaperLib; -import io.papermc.lib.environments.CraftBukkitEnvironment; -import io.papermc.lib.environments.Environment; import world.bentobox.bentobox.AbstractCommonSetup; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; @@ -148,8 +145,6 @@ public class IslandsManagerTest extends AbstractCommonSetup { private Material sign; private Material wallSign; - private Environment env; - // Class under test IslandsManager im; @@ -170,7 +165,7 @@ public static void beforeClass() throws IllegalAccessException, InvocationTarget } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "deprecation" }) @Before public void setUp() throws Exception { super.setUp(); @@ -350,10 +345,6 @@ public void setUp() throws Exception { sign = Material.BIRCH_SIGN; wallSign = Material.ACACIA_WALL_SIGN; - // PaperLib - env = new CraftBukkitEnvironment(); - PaperLib.setCustomEnvironment(env); - // Util strip spaces when(Util.stripSpaceAfterColorCodes(anyString())).thenCallRealMethod(); @@ -423,7 +414,6 @@ public void testIsSafeLocationSubmerged() { assertFalse(im.isSafeLocation(location)); } - @SuppressWarnings("deprecation") @Test @Ignore("Material#isSolid() cannot be tested") public void testCheckIfSafeTrapdoor() { diff --git a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java index d550ca413..1716a5394 100644 --- a/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java @@ -181,7 +181,7 @@ public void setUp() throws Exception { when(p.getUniqueId()).thenReturn(uuid); AttributeInstance at = mock(AttributeInstance.class); when(at.getValue()).thenReturn(20D); - when(p.getAttribute(Attribute.MAX_HEALTH)).thenReturn(at); + when(p.getAttribute(Attribute.GENERIC_MAX_HEALTH)).thenReturn(at); when(p.getName()).thenReturn("tastybento"); User.getInstance(p); diff --git a/src/test/java/world/bentobox/bentobox/mocks/ServerMocks.java b/src/test/java/world/bentobox/bentobox/mocks/ServerMocks.java index 8fd039318..282caceba 100644 --- a/src/test/java/world/bentobox/bentobox/mocks/ServerMocks.java +++ b/src/test/java/world/bentobox/bentobox/mocks/ServerMocks.java @@ -1,5 +1,6 @@ package world.bentobox.bentobox.mocks; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -21,10 +22,26 @@ import org.bukkit.Tag; import org.bukkit.UnsafeValues; import org.eclipse.jdt.annotation.NonNull; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import io.papermc.paper.ServerBuildInfo; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(ServerBuildInfo.class) public final class ServerMocks { + @Mock + private static ServerBuildInfo sbi; public static @NonNull Server newServer() { + PowerMockito.mockStatic(ServerBuildInfo.class, Mockito.RETURNS_MOCKS); + when(sbi.asString(any())).thenReturn("Mock server version"); + when(ServerBuildInfo.buildInfo()).thenReturn(sbi); + Server mock = mock(Server.class); Logger noOp = mock(Logger.class); @@ -66,7 +83,7 @@ public final class ServerMocks { doReturn(key).when(keyed).getKey(); return keyed; }); - }).when(registry).get(notNull()); + }).when(registry).get((NamespacedKey) notNull()); return registry; })).when(mock).getRegistry(notNull()); diff --git a/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java b/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java index aa0b06016..f0101694b 100644 --- a/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/BlueprintManagementPanelTest.java @@ -80,7 +80,7 @@ public void setUp() throws Exception { ItemFactory itemFac = mock(ItemFactory.class); when(Bukkit.getItemFactory()).thenReturn(itemFac); // Panel inventory - when(Bukkit.createInventory(any(), Mockito.anyInt(), any())).thenReturn(inv); + when(Bukkit.createInventory(any(), Mockito.anyInt(), anyString())).thenReturn(inv); // Player Player player = mock(Player.class); diff --git a/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java b/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java index 0f8bb9433..c94653483 100644 --- a/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/customizable/IslandCreationPanelTest.java @@ -37,6 +37,7 @@ import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; +import io.papermc.paper.ServerBuildInfo; import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.addons.GameModeAddon; @@ -48,13 +49,14 @@ import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.managers.PlayersManager; +import world.bentobox.bentobox.mocks.ServerMocks; /** * @author tastybento * */ @RunWith(PowerMockRunner.class) -@PrepareForTest({ Bukkit.class, BentoBox.class }) +@PrepareForTest({ Bukkit.class, BentoBox.class, ServerBuildInfo.class }) public class IslandCreationPanelTest { @Mock @@ -89,6 +91,8 @@ public class IslandCreationPanelTest { */ @Before public void setUp() throws Exception { + ServerMocks.newServer(); + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); // Set up plugin @@ -160,7 +164,8 @@ public void setUp() throws Exception { when(plugin.getIWM()).thenReturn(iwm); // Panel inventory - when(Bukkit.createInventory(any(), Mockito.anyInt(), any())).thenReturn(inv); + + when(Bukkit.createInventory(any(), Mockito.anyInt(), anyString())).thenReturn(inv); // Item Factory (needed for ItemStack) ItemFactory itemF = mock(ItemFactory.class); @@ -202,6 +207,7 @@ public void setUp() throws Exception { public void tearDown() { User.clearUsers(); Mockito.framework().clearInlineMocks(); + ServerMocks.unsetBukkitServer(); } /** diff --git a/src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java b/src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java index 2a2ad5526..7b8639bda 100644 --- a/src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java +++ b/src/test/java/world/bentobox/bentobox/panels/customizable/LanguagePanelTest.java @@ -131,7 +131,7 @@ public void setUp() throws Exception { // Panel PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); - when(Bukkit.createInventory(any(), Mockito.anyInt(), any())).thenReturn(inv); + when(Bukkit.createInventory(any(), Mockito.anyInt(), anyString())).thenReturn(inv); // Item Factory (needed for ItemStack) ItemFactory itemF = mock(ItemFactory.class); diff --git a/src/test/java/world/bentobox/bentobox/util/DefaultPasteUtilTest.java b/src/test/java/world/bentobox/bentobox/util/DefaultPasteUtilTest.java index 38135c66d..768b1ad32 100644 --- a/src/test/java/world/bentobox/bentobox/util/DefaultPasteUtilTest.java +++ b/src/test/java/world/bentobox/bentobox/util/DefaultPasteUtilTest.java @@ -141,6 +141,9 @@ public void setUp() throws Exception { when(plugin.getHooks()).thenReturn(hooksManager); when(plugin.getPlayers()).thenReturn(pm); + + // Blueprint Entity + when(blueprintEntity.getType()).thenReturn(EntityType.PLAYER); } /** diff --git a/src/test/java/world/bentobox/bentobox/util/UtilTest.java b/src/test/java/world/bentobox/bentobox/util/UtilTest.java index 4745b6ce3..d6162354d 100644 --- a/src/test/java/world/bentobox/bentobox/util/UtilTest.java +++ b/src/test/java/world/bentobox/bentobox/util/UtilTest.java @@ -494,4 +494,112 @@ public void testTranslateColorCodesHex() { assertEquals("§x§f§f§0§0§0§0full hex", Util.translateColorCodes("&#ff0000 full hex")); assertEquals("&#ggg outside hex range", Util.translateColorCodes("&#ggg outside hex range")); } -} + + /** + * Tests if the method returns true for identical versions without SNAPSHOT. + */ + @Test + public void testVersionIsCompatible_SameVersion() { + assertTrue("Same versions should be compatible", Util.isVersionCompatible("2.0.0", "2.0.0")); + } + + /** + * Tests if the method returns true for identical SNAPSHOT versions. + */ + @Test + public void testVersionIsCompatible_SnapshotToSnapshot() { + assertTrue("Same SNAPSHOT versions should be compatible", + Util.isVersionCompatible("2.0.0-SNAPSHOT", "2.0.0-SNAPSHOT")); + } + + /** + * Tests if the method considers release versions compatible with their SNAPSHOT equivalents. + */ + @Test + public void testVersionIsCompatible_ReleaseGreaterThanSnapshot() { + assertTrue("Release version should be compatible with SNAPSHOT of the same version", + Util.isVersionCompatible("2.0.0", "2.0.0-SNAPSHOT")); + } + + /** + * Tests if the method considers SNAPSHOT versions less compatible than release versions. + */ + @Test + public void testVersionIsCompatible_SnapshotLessThanRelease() { + assertFalse("SNAPSHOT version should not be compatible with release of the same version", + Util.isVersionCompatible("2.0.0-SNAPSHOT", "2.0.0")); + } + + /** + * Tests if the method correctly identifies compatibility for a higher major version. + */ + @Test + public void testVersionIsCompatible_MajorVersionGreater() { + assertTrue("Higher major version should be compatible", Util.isVersionCompatible("3.0.0", "2.0.0")); + } + + /** + * Tests if the method correctly identifies incompatibility for a lower major version. + */ + @Test + public void testVersionIsCompatible_MajorVersionLower() { + assertFalse("Lower major version should not be compatible", Util.isVersionCompatible("1.9.9", "2.0.0")); + } + + /** + * Tests if the method correctly identifies compatibility for a higher minor version. + */ + @Test + public void testVersionIsCompatible_MinorVersionGreater() { + assertTrue("Higher minor version should be compatible", Util.isVersionCompatible("2.1.0", "2.0.0")); + } + + /** + * Tests if the method correctly identifies incompatibility for a lower minor version. + */ + @Test + public void testVersionIsCompatible_MinorVersionLower() { + assertFalse("Lower minor version should not be compatible", Util.isVersionCompatible("2.0.0", "2.1.0")); + } + + /** + * Tests if the method correctly identifies compatibility for a higher patch version. + */ + @Test + public void testVersionIsCompatible_PatchVersionGreater() { + assertTrue("Higher patch version should be compatible", Util.isVersionCompatible("2.0.1", "2.0.0")); + } + + /** + * Tests if the method correctly identifies incompatibility for a lower patch version. + */ + @Test + public void testVersionIsCompatible_PatchVersionLower() { + assertFalse("Lower patch version should not be compatible", Util.isVersionCompatible("2.0.0", "2.0.1")); + } + + /** + * Tests if the method correctly handles compatibility when both versions have a SNAPSHOT suffix. + */ + @Test + public void testVersionIsCompatible_HandlesSnapshotSuffix() { + assertTrue("Higher patch version (SNAPSHOT) should be compatible with lower patch version (SNAPSHOT)", + Util.isVersionCompatible("2.0.1-SNAPSHOT", "2.0.0-SNAPSHOT")); + } + + /** + * Tests if the method throws an exception for an empty version string. + */ + @Test(expected = NumberFormatException.class) + public void testVersionIsCompatible_EmptyVersion() { + Util.isVersionCompatible("", "2.0.0"); + } + + /** + * Tests if the method throws an exception for a null version string. + */ + @Test(expected = NullPointerException.class) + public void testVersionIsCompatible_NullVersion() { + Util.isVersionCompatible(null, "2.0.0"); + } +} \ No newline at end of file