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<String, JSONObject> 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<Entity> 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;
}
}