From 9ab9d7f11b834cd8c61767c9c53a353ecd0134c3 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 30 Nov 2024 07:59:31 -0800 Subject: [PATCH] Citizen's hook WIP --- pom.xml | 21 ++- .../world/bentobox/bentobox/BentoBox.java | 4 + .../blueprints/BlueprintClipboard.java | 42 +++-- .../bentobox/blueprints/BlueprintPaster.java | 28 ++- .../dataobjects/BlueprintEntity.java | 38 ++++ .../bentobox/bentobox/hooks/CitizensHook.java | 165 ++++++++++++++++++ .../bentobox/util/DefaultPasteUtil.java | 17 +- 7 files changed, 299 insertions(+), 16 deletions(-) create mode 100644 src/main/java/world/bentobox/bentobox/hooks/CitizensHook.java diff --git a/pom.xml b/pom.xml index 90bf01ed9..ed5c97230 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,7 @@ -LOCAL - 3.0.1 + 3.1.0 bentobox-world https://sonarcloud.io ${project.basedir}/lib @@ -192,6 +192,11 @@ clojars https://repo.clojars.org/ + + + citizens-repo + https://maven.citizensnpcs.co/repo + @@ -387,6 +392,20 @@ 1.1.13 compile + + + net.citizensnpcs + citizens-main + 2.0.35-SNAPSHOT + jar + provided + + + * + * + + + diff --git a/src/main/java/world/bentobox/bentobox/BentoBox.java b/src/main/java/world/bentobox/bentobox/BentoBox.java index daa0b91dd..ae4f06a60 100644 --- a/src/main/java/world/bentobox/bentobox/BentoBox.java +++ b/src/main/java/world/bentobox/bentobox/BentoBox.java @@ -24,6 +24,7 @@ import world.bentobox.bentobox.api.user.Notifier; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.commands.BentoBoxCommand; import world.bentobox.bentobox.database.DatabaseSetup; +import world.bentobox.bentobox.hooks.CitizensHook; import world.bentobox.bentobox.hooks.ItemsAdderHook; import world.bentobox.bentobox.hooks.MultipaperHook; import world.bentobox.bentobox.hooks.MultiverseCoreHook; @@ -192,6 +193,9 @@ public class BentoBox extends JavaPlugin implements Listener { hooksManager.registerHook(new VaultHook()); + // Citizens + hooksManager.registerHook(new CitizensHook()); + // MythicMobs hooksManager.registerHook(new MythicMobsHook()); diff --git a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java index d189f2df0..315ef66e8 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintClipboard.java @@ -2,7 +2,6 @@ package world.bentobox.bentobox.blueprints; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -23,8 +22,8 @@ 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.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Tameable; import org.bukkit.entity.Villager; @@ -44,6 +43,7 @@ import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; +import world.bentobox.bentobox.hooks.CitizensHook; import world.bentobox.bentobox.hooks.MythicMobsHook; /** @@ -70,20 +70,21 @@ public class BlueprintClipboard { private final Map bpBlocks = new LinkedHashMap<>(); private final BentoBox plugin = BentoBox.getInstance(); private Optional mmh; + private Optional ch; /** * Create a clipboard for blueprint * @param blueprint - the blueprint to load into the clipboard */ public BlueprintClipboard(@NonNull Blueprint blueprint) { + this(); this.blueprint = blueprint; - // MythicMobs - mmh = plugin.getHooks().getHook("MythicMobs").filter(MythicMobsHook.class::isInstance) - .map(MythicMobsHook.class::cast); } public BlueprintClipboard() { - // MythicMobs + // Citizens Hook + ch = plugin.getHooks().getHook("Citizens").filter(CitizensHook.class::isInstance).map(CitizensHook.class::cast); + // MythicMobs Hook mmh = plugin.getHooks().getHook("MythicMobs").filter(MythicMobsHook.class::isInstance) .map(MythicMobsHook.class::cast); } @@ -136,13 +137,20 @@ public class BlueprintClipboard { private void copyAsync(World world, User user, List vectorsToCopy, int speed, boolean copyAir, boolean copyBiome) { copying = false; + // Citizens + if (ch.isPresent()) { + // Add all the citizens for the area in one go. This is pretty fast. + bpEntities.putAll(ch.get().getCitizensInArea(world, vectorsToCopy, origin)); + } + + // Repeating copy task copyTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { if (copying) { return; } copying = true; vectorsToCopy.stream().skip(index).limit(speed).forEach(v -> { - List ents = world.getLivingEntities().stream() + List ents = world.getEntities().stream() .filter(Objects::nonNull) .filter(e -> !(e instanceof Player)) .filter(e -> new Vector(Math.rint(e.getLocation().getX()), @@ -153,6 +161,7 @@ public class BlueprintClipboard { count++; } }); + index += speed; int percent = (int)(index * 100 / (double)vectorsToCopy.size()); if (percent != lastPercentage && percent % 10 == 0) { @@ -189,9 +198,9 @@ public class BlueprintClipboard { return r; } - private boolean copyBlock(Location l, boolean copyAir, boolean copyBiome, Collection entities) { + private boolean copyBlock(Location l, boolean copyAir, boolean copyBiome, List ents) { Block block = l.getBlock(); - if (!copyAir && block.getType().equals(Material.AIR) && entities.isEmpty()) { + if (!copyAir && block.getType().equals(Material.AIR) && ents.isEmpty()) { return false; } // Create position @@ -202,14 +211,14 @@ public class BlueprintClipboard { Vector pos = new Vector(x, y, z); // Set entities - List bpEnts = setEntities(entities); + List bpEnts = setEntities(ents); // Store if (!bpEnts.isEmpty()) { bpEntities.put(pos, bpEnts); } // Return if this is just air block - if (!copyAir && block.getType().equals(Material.AIR) && !entities.isEmpty()) { + if (!copyAir && block.getType().equals(Material.AIR) && !ents.isEmpty()) { return true; } @@ -291,9 +300,15 @@ public class BlueprintClipboard { return cs; } - private List setEntities(Collection entities) { + /** + * Deals with any entities that are in this block. Technically, this could be more than one, but is usually one. + * @param ents collection of entities + * @return Serialized list of entities + */ + private List setEntities(List ents) { List bpEnts = new ArrayList<>(); - for (LivingEntity entity: entities) { + for (Entity entity : ents) { + BentoBox.getInstance().logDebug("Etity = " + entity); BlueprintEntity bpe = new BlueprintEntity(); bpe.setType(entity.getType()); @@ -329,6 +344,7 @@ public class BlueprintClipboard { bpe.setStyle(horse.getStyle()); } + // Mythic mob check mmh.filter(mm -> mm.isMythicMob(entity)).map(mm -> mm.getMythicMob(entity)) .ifPresent(bpe::setMythicMobsRecord); diff --git a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java index d62f8807d..8ba1cb498 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/BlueprintPaster.java @@ -124,12 +124,38 @@ public class BlueprintPaster { location.setY(y); } - private record Bits(Map blocks, + /** + * A record of all the "bits" of the blueprint that need to be pasted + * Consists of blocks, attached blocks, entities, iterators for the blocks and a speed + */ + private record Bits( + /** + * Basic blocks to the pasted (not attached blocks) + */ + Map blocks, + /** + * Attached blocks + */ Map attached, + /** + * Entities to be pasted + */ Map> entities, + /** + * Basic block pasting iterator + */ Iterator> it, + /** + * Attached block pasting iterator + */ Iterator> it2, + /** + * Entity pasting iterator + */ Iterator>> it3, + /** + * Paste speed + */ int pasteSpeed) {} /** 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 4c4eb1de1..3c1e1959a 100644 --- a/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java +++ b/src/main/java/world/bentobox/bentobox/blueprints/dataobjects/BlueprintEntity.java @@ -24,6 +24,11 @@ import com.google.gson.annotations.Expose; */ public class BlueprintEntity { + // Citizens storage + @Expose + private String citizen; + + // MythicMobs storage public record MythicMobRecord(String type, String displayName, double level, float power, String stance) { } @@ -303,5 +308,38 @@ public class BlueprintEntity { this.MMStance = mmr.stance(); this.MMpower = mmr.power(); } + + /** + * @return the citizen + */ + public String getCitizen() { + return citizen; + } + + /** + * @param citizen the citizen to set + */ + public void setCitizen(String citizen) { + this.citizen = citizen; + } + + @Override + public String toString() { + return "BlueprintEntity [" + (citizen != null ? "citizen=" + citizen + ", " : "") + + (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 : "") + "]"; + } } diff --git a/src/main/java/world/bentobox/bentobox/hooks/CitizensHook.java b/src/main/java/world/bentobox/bentobox/hooks/CitizensHook.java new file mode 100644 index 000000000..ba64af478 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/hooks/CitizensHook.java @@ -0,0 +1,165 @@ +package world.bentobox.bentobox.hooks; + +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.EntityType; +import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.Nullable; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.npc.NPCRegistry; +import net.citizensnpcs.api.util.MemoryDataKey; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.hooks.Hook; +import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; + +/** + * Provides copy and pasting of Citizens in blueprints + * + * @author tastybento + * @since 3.1.0 + */ +public class CitizensHook extends Hook { + + MemoryDataKey dataKeyTest = new MemoryDataKey(); + + public CitizensHook() { + super("Citizens", Material.PLAYER_HEAD); + } + + public String serializeNPC(NPC npc) { + if (npc == null) { + throw new IllegalArgumentException("NPC cannot be null."); + } + MemoryDataKey dataKey = new MemoryDataKey(); + npc.save(dataKey); // Save NPC data into the MemoryDataKey + // Convert MemoryDataKey to a YAML string + YamlConfiguration yaml = new YamlConfiguration(); + for (Entry en : dataKey.getValuesDeep().entrySet()) { + BentoBox.getInstance().logDebug("Serial key = " + en.getKey() + " = " + en.getValue()); + yaml.set(en.getKey(), en.getValue()); + } + dataKeyTest = dataKey; + return yaml.saveToString(); + } + + /** + * Get a map of serialized Citizens that are in a set of locations + * @param world world where this occurs + * @param vectorsToCopy list of locations in that world as vectors + * @param origin + * @return map + */ + public Map> getCitizensInArea(World world, List vectorsToCopy, + @Nullable Vector origin) { + Map> bpEntities = new HashMap<>(); + for (NPC npc : CitizensAPI.getNPCRegistry()) { + if (npc.isSpawned()) { + Location npcLocation = npc.getEntity().getLocation(); + Vector spot = new Vector(npcLocation.getBlockX(), npcLocation.getBlockY(), npcLocation.getBlockZ()); + if (npcLocation.getWorld().equals(world) && vectorsToCopy.contains(spot)) { + BlueprintEntity cit = new BlueprintEntity(); + cit.setType(npc.getEntity().getType()); // Must be set to be pasted + cit.setCitizen(serializeNPC(npc)); + // Retrieve or create the list, then add the entity + List entities = bpEntities.getOrDefault(spot, new ArrayList<>()); + entities.add(cit); + // Create position + Vector origin2 = origin == null ? new Vector(0, 0, 0) : origin; + int x = spot.getBlockX() - origin2.getBlockX(); + int y = spot.getBlockY() - origin2.getBlockY(); + int z = spot.getBlockZ() - origin2.getBlockZ(); + Vector pos = new Vector(x, y, z); + // Store + bpEntities.put(pos, entities); // Update the map + } + } + } + return bpEntities; + } + + /** + * Spawn a Citizen + * @param serializedData serialized data + * @param location location + * @return true if spawn is successful + */ + public boolean spawnCitizen(EntityType type, String serializedData, Location location) { + BentoBox.getInstance().logDebug("spawn Citizen at " + location + " with this data " + serializedData); + if (serializedData == null || location == null) { + throw new IllegalArgumentException("Serialized data and location cannot be null."); + } + + // Load the serialized data into a YamlConfiguration + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(new StringReader(serializedData)); + + // Create a new MemoryDataKey from the loaded data + MemoryDataKey dataKey = new MemoryDataKey(); + for (String key : yaml.getKeys(true)) { + BentoBox.getInstance().logDebug("data key " + key + " = " + yaml.get(key)); + if (key.equalsIgnoreCase("metadata") || key.equalsIgnoreCase("traits") + || key.equalsIgnoreCase("traits.owner") || key.equalsIgnoreCase("traits.location") + || key.equalsIgnoreCase("navigator")) { + continue; + } + dataKey.setRaw(key, yaml.get(key)); + } + + // Get the NPC details from the serialized data + String name = dataKey.getString("name"); + //String type = dataKey.getString("traits.type"); + + BentoBox.getInstance().logDebug("Entity type = " + type + " name = " + name); + if (type == null) { + // No luck + return false; + } + // Create a new NPC and load the data + BentoBox.getInstance().logDebug("Create a new NPC and load the data"); + NPCRegistry registry = CitizensAPI.getNPCRegistry(); + try { + NPC npc = registry.createNPC(type, name); + + npc.load(dataKey); // Load the serialized data into the NPC + for (Entry en : dataKey.getValuesDeep().entrySet()) { + BentoBox.getInstance().logDebug("loaded key " + en.getKey() + " = " + en.getValue()); + } + boolean r = npc.spawn(location); // Spawn the NPC at the specified location + BentoBox.getInstance().logDebug("Spawn = " + r); + if (!r) { + npc.load(dataKeyTest); + BentoBox.getInstance().logDebug(npc.spawn(location)); // Spawn the NPC at the specified location + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + + @Override + public boolean hook() { + boolean hooked = this.isPluginAvailable(); + if (!hooked) { + BentoBox.getInstance().logError("Could not hook into Citizens"); + } + return hooked; // The hook process shouldn't fail + } + + @Override + public String getFailureCause() { + return null; // The hook process shouldn't fail + } +} diff --git a/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java b/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java index 76fb68be9..c22ba05fe 100644 --- a/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java +++ b/src/main/java/world/bentobox/bentobox/util/DefaultPasteUtil.java @@ -33,6 +33,7 @@ import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBlock; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintCreatureSpawner; import world.bentobox.bentobox.blueprints.dataobjects.BlueprintEntity; import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.hooks.CitizensHook; import world.bentobox.bentobox.hooks.MythicMobsHook; import world.bentobox.bentobox.nms.PasteHandler; @@ -169,8 +170,11 @@ public class DefaultPasteUtil { * @param island - island * @param location - location * @param list - blueprint entities + * @return future boolean - true if Bukkit entity spawned, false another plugin entity spawned */ public static CompletableFuture setEntity(Island island, Location location, List list) { + BentoBox.getInstance().logDebug("List of entities to paste at " + location); + list.forEach(bpe -> BentoBox.getInstance().logDebug(bpe)); World world = location.getWorld(); assert world != null; return Util.getChunkAtAsync(location).thenRun(() -> list.stream().filter(k -> k.getType() != null) @@ -182,9 +186,20 @@ public class DefaultPasteUtil { * @param k the blueprint entity definition * @param location location * @param island island - * @return true if Bukkit entity spawned, false if MythicMob entity spawned + * @return true if Bukkit entity spawned, false another plugin entity spawned */ static boolean spawnBlueprintEntity(BlueprintEntity k, Location location, Island island) { + BentoBox.getInstance().logDebug("pasting entity " + k.getType() + " at " + location); + // Citizens entity + if (k.getCitizen() != null && plugin.getHooks().getHook("Citizens").filter(mmh -> mmh instanceof CitizensHook) + .map(mmh -> ((CitizensHook) mmh).spawnCitizen(k.getType(), k.getCitizen(), location)).orElse(false)) { + BentoBox.getInstance().logDebug("Citizen spawning done"); + // Citizen has spawned. + return false; + } else { + BentoBox.getInstance().logDebug("Citizen spawning failed"); + } + // Mythic Mobs entity if (k.getMythicMobsRecord() != null && plugin.getHooks().getHook("MythicMobs") .filter(mmh -> mmh instanceof MythicMobsHook) .map(mmh -> ((MythicMobsHook) mmh).spawnMythicMob(k.getMythicMobsRecord(), location))