package org.mineacademy.boss.hook; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.mineacademy.boss.model.Boss; import org.mineacademy.boss.model.BossCitizensSettings; import org.mineacademy.boss.model.BossRegion; import org.mineacademy.boss.model.SpawnedBoss; import org.mineacademy.fo.Common; import org.mineacademy.fo.MinecraftVersion; import org.mineacademy.fo.MinecraftVersion.V; import org.mineacademy.fo.PlayerUtil; import org.mineacademy.fo.collection.expiringmap.ExpiringMap; import org.mineacademy.fo.jsonsimple.JSONObject; import org.mineacademy.fo.jsonsimple.JSONParser; import org.mineacademy.fo.remain.CompEquipmentSlot; import org.mineacademy.fo.remain.Remain; import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.ai.EntityTarget; import net.citizensnpcs.api.ai.Goal; import net.citizensnpcs.api.ai.Navigator; import net.citizensnpcs.api.ai.goals.WanderGoal; import net.citizensnpcs.api.npc.MetadataStore; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.trait.trait.Equipment; import net.citizensnpcs.api.trait.trait.Equipment.EquipmentSlot; import net.citizensnpcs.trait.SkinTrait; /** * Connector to Citizens for integration. */ public final class CitizensHook { /* * Skin cache */ private static final Map cache = ExpiringMap.builder().expiration(30, TimeUnit.MINUTES).build(); /** * Register our custom traits */ public static void registerTraits() { if (MinecraftVersion.atLeast(V.v1_9)) try { net.citizensnpcs.api.CitizensAPI.getTraitFactory().registerTrait(net.citizensnpcs.api.trait.TraitInfo.create(MaxHealthTrait.class)); } catch (final Throwable t) { // Ignore, no implementation for the current MC version } } /** * Spawns the given Boss at the location with help of Citizens * * @param boss * @param location * @return */ public static Entity spawn(Boss boss, Location location) { final String alias = boss.getAlias(); final NPC npc = CitizensAPI.getNPCRegistry().createNPC(boss.getType(), alias.length() > 16 ? alias.substring(0, 16) : alias); // Apply attributes update(boss, npc); // Spawn if (!npc.spawn(location)) { npc.destroy(); return null; } // Set initial health to max and save ((LivingEntity) npc.getEntity()).setHealth(boss.getMaxHealth()); final MaxHealthTrait trait = npc.getOrAddTrait(MaxHealthTrait.class); if (trait != null) trait.health = ((LivingEntity) npc.getEntity()).getHealth(); return npc.getEntity(); } /** * Updates Boss behavior for the spawned Boss entity * * @param boss * @param entity */ public static void update(Boss boss, Entity entity) { final NPC npc = CitizensAPI.getNPCRegistry().getNPC(entity); if (npc == null) return; if (!npc.isSpawned()) npc.destroy(); update(boss, npc); } /** * Updates Boss behavior for the spawned Boss entity * * @param boss * @param npc */ public static void update(Boss boss, NPC npc) { final BossCitizensSettings citizens = boss.getCitizensSettings(); final MetadataStore data = npc.data(); npc.setProtected(false); npc.getOrAddTrait(MaxHealthTrait.class); final Equipment equipment = npc.getOrAddTrait(Equipment.class); for (final CompEquipmentSlot slot : CompEquipmentSlot.values()) { final ItemStack item = boss.getEquipmentItem(slot); equipment.set(EquipmentSlot.valueOf(slot.getBukkitName()), item); } data.setPersistent("BossName", boss.getName()); if (citizens.getDeathSound() != null) data.setPersistent(NPC.Metadata.DEATH_SOUND, citizens.getDeathSound()); if (citizens.getHurtSound() != null) data.setPersistent(NPC.Metadata.HURT_SOUND, citizens.getHurtSound()); if (citizens.getAmbientSound() != null) data.setPersistent(NPC.Metadata.AMBIENT_SOUND, citizens.getAmbientSound()); data.setPersistent(NPC.Metadata.DROPS_ITEMS, true); data.setPersistent(NPC.Metadata.COLLIDABLE, true); data.setPersistent(NPC.Metadata.DAMAGE_OTHERS, true); if (citizens.isTargetGoalEnabled()) { final Goal goal = BossTargetNearbyEntityGoal.builder(npc) .aggressive(citizens.isTargetGoalAggressive()) .radius(citizens.getTargetGoalRadius()) .targets(citizens.getTargetGoalEntities()) .build(); npc.getDefaultGoalController().addGoal(goal, 1); } if (citizens.isWanderGoalEnabled()) npc.getDefaultGoalController().addGoal(WanderGoal.builder(npc).xrange(citizens.getWanderGoalRadius()).yrange(citizens.getWanderGoalRadius()).build(), citizens.getWanderGoalRadius()); else npc.getDefaultGoalController().cancelCurrentExecution(); final Navigator gps = npc.getNavigator(); // Fix weird slow motion issue if (boss.getType() == EntityType.PLAYER) { gps.getLocalParameters().speedModifier(1F); gps.getLocalParameters().speed(1F); gps.getLocalParameters().baseSpeed(4F); } // https://github.com/kangarko/Boss/issues/1195#issuecomment-1692729888 gps.getLocalParameters().distanceMargin(0.5).pathDistanceMargin(0.5); // Fallback to MC AI if (!citizens.isTargetGoalEnabled() && !citizens.isWanderGoalEnabled()) npc.setUseMinecraftAI(true); // Remove teleporting to players when far away npc.getNavigator().getDefaultParameters().stuckAction(null); // Set skin and finalize setSkinUrl(boss, npc); } /* * Helper to set Skin URL */ private static void setSkinUrl(Boss boss, NPC npc) { final String skin = boss.getCitizensSettings().getSkinOrAlias(); final SkinTrait trait = npc.getOrAddTrait(SkinTrait.class); if (skin.startsWith("http://") || skin.startsWith("https://")) { Common.runAsync(() -> { final JSONObject output = cache.get(skin); if (output != null) { setSkin0(npc, trait, cache.get(skin)); return; } try { final URL target = new URL("https://api.mineskin.org/generate/url"); final HttpURLConnection connection = (HttpURLConnection) target.openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setConnectTimeout(2_000); connection.setReadTimeout(5_000); try (DataOutputStream out = new DataOutputStream(connection.getOutputStream())) { out.writeBytes("url=" + URLEncoder.encode(skin, "UTF-8")); out.close(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { final JSONObject newOutput = (JSONObject) JSONParser.deserialize(reader); connection.disconnect(); Common.runLater(() -> { setSkin0(npc, trait, newOutput); cache.put(skin, newOutput); }); } } } catch (final Throwable t) { Common.error(t, "Failed fetching NPC's " + npc.getName() + " skin from URL, check if it is valid.", "URL:" + skin); } }); } else trait.setSkinName(skin, true); } /* * Helper method to set the persistent skin trait */ private static void setSkin0(NPC npc, SkinTrait trait, JSONObject output) { final JSONObject data = (JSONObject) output.get("data"); final String uuid = (String) data.get("uuid"); final JSONObject texture = (JSONObject) data.get("texture"); final String textureEncoded = (String) texture.get("value"); final String signature = (String) texture.get("signature"); trait.setSkinPersistent(uuid, signature, textureEncoded); } /** * Attempts to destroy NPC associated with the given NPC * * @param entity */ public static void destroy(Entity entity) { final NPC npc = CitizensAPI.getNPCRegistry().getNPC(entity); if (npc != null) npc.destroy(); } /** * Retargets the closest entity for the given boss * * @param spawnedBoss the boss */ public static void retarget(SpawnedBoss spawnedBoss) { final Entity entity = spawnedBoss.getEntity(); final Boss boss = spawnedBoss.getBoss(); final BossRegion spawnRegion = boss.isKeptInSpawnRegion() ? spawnedBoss.getSpawnRegion() : null; final NPC npc = CitizensAPI.getNPCRegistry().getNPC(entity); final BossCitizensSettings citizens = boss.getCitizensSettings(); if (npc != null && citizens.isTargetGoalEnabled()) { final List potentialTargets = new ArrayList<>(); final EntityTarget oldTarget = npc.getNavigator().getEntityTarget(); for (final Entity nearby : Remain.getNearbyEntities(entity.getLocation(), citizens.getTargetGoalRadius())) { // Ignore same type entities if (entity.getType() == nearby.getType()) continue; // Do not target self if (entity.getUniqueId().equals(nearby.getUniqueId())) continue; // Only retarget to someone else if (oldTarget != null && oldTarget.getTarget().getUniqueId().equals(nearby.getUniqueId())) continue; // Ignore entities out of reach if (spawnRegion != null && !spawnRegion.isWithin(nearby.getLocation())) continue; if (nearby instanceof Player) { final Player player = (Player) nearby; if (player.getGameMode() != GameMode.SURVIVAL || PlayerUtil.isVanished(player)) continue; } final SpawnedBoss nearbyBoss = Boss.findBoss(nearby); if (nearbyBoss != null && nearbyBoss.getBoss().equals(boss)) continue; if (citizens.getTargetGoalEntities().contains(nearby.getType())) potentialTargets.add(nearby); } if (!potentialTargets.isEmpty()) { Collections.shuffle(potentialTargets); npc.getNavigator().setTarget(potentialTargets.get(0), citizens.isTargetGoalAggressive()); } } // Remove teleporting to players when far away if (npc != null) npc.getNavigator().getDefaultParameters().stuckAction(null); } /** * Sends the given Boss to the given location * * @param boss * @param target */ public static void sendToLocation(SpawnedBoss boss, Location target) { final NPC npc = CitizensAPI.getNPCRegistry().getNPC(boss.getEntity()); if (npc != null) npc.getNavigator().setTarget(target); } /** * Attempts to find a boss from the given NPC. * * @param entity * @return */ @Nullable public static SpawnedBoss findNPC(Entity entity) { final NPC npc = CitizensAPI.getNPCRegistry().getNPC(entity); if (npc != null) { final String bossName = npc.data().get("BossName"); if (bossName != null) { final Boss boss = Boss.findBoss(bossName); if (boss != null) return new SpawnedBoss(boss, (LivingEntity) entity); } } return null; } }