diff --git a/src/main/java/net/citizensnpcs/EventListen.java b/src/main/java/net/citizensnpcs/EventListen.java index 372460782..05dfcc020 100644 --- a/src/main/java/net/citizensnpcs/EventListen.java +++ b/src/main/java/net/citizensnpcs/EventListen.java @@ -34,6 +34,7 @@ import net.citizensnpcs.api.event.NPCDeathEvent; import net.citizensnpcs.api.event.NPCDespawnEvent; import net.citizensnpcs.api.event.NPCLeftClickEvent; import net.citizensnpcs.api.event.NPCRightClickEvent; +import net.citizensnpcs.api.event.NPCSpawnEvent; import net.citizensnpcs.api.event.PlayerCreateNPCEvent; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPCRegistry; @@ -81,6 +82,7 @@ import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldUnloadEvent; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; import org.bukkit.scoreboard.Team; public class EventListen implements Listener { @@ -322,6 +324,16 @@ public class EventListen implements Listener { toRespawn.put(coord, event.getNPC()); } + @EventHandler + public void onNPCSpawn(NPCSpawnEvent event) { + SkinnableEntity skinnable = NMS.getSkinnable(event.getNPC().getEntity()); + if (skinnable == null) + return; + + // reset nearby players in case they are not looking at the NPC when it spawns. + resetNearbyPlayers(skinnable); + } + @EventHandler public void onNPCDespawn(NPCDespawnEvent event) { if (event.getReason() == DespawnReason.PLUGIN || event.getReason() == DespawnReason.REMOVAL) { @@ -441,7 +453,6 @@ public class EventListen implements Listener { // a player moves a certain distance from their last position. @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerMove(final PlayerMoveEvent event) { - SkinUpdateTracker updateTracker = skinUpdateTrackers.get(event.getPlayer().getUniqueId()); if (updateTracker == null) return; @@ -460,17 +471,25 @@ public class EventListen implements Listener { if (player.hasMetadata("NPC")) continue; - skinUpdateTrackers.put(player.getUniqueId(), new SkinUpdateTracker(player)); + SkinUpdateTracker tracker = skinUpdateTrackers.get(player.getUniqueId()); + if (tracker == null) + continue; + + tracker.hardReset(player); } } - public void recalculatePlayer(final Player player, long delay, final boolean isInitial) { - + public void recalculatePlayer(final Player player, long delay, boolean reset) { if (player.hasMetadata("NPC")) return; - if (isInitial) { - skinUpdateTrackers.put(player.getUniqueId(), new SkinUpdateTracker(player)); + SkinUpdateTracker tracker = skinUpdateTrackers.get(player.getUniqueId()); + if (tracker == null) { + tracker = new SkinUpdateTracker(player); + skinUpdateTrackers.put(player.getUniqueId(), tracker); + } + else if (reset) { + tracker.hardReset(player); } new BukkitRunnable() { @@ -480,20 +499,42 @@ public class EventListen implements Listener { List nearbyNPCs = getNearbySkinnableNPCs(player); for (SkinnableEntity npc : nearbyNPCs) { - npc.getSkinTracker().updateViewer(player); - } - - if (!nearbyNPCs.isEmpty() && isInitial) { - // one more time to help when resource pack load times - // prevent immediate skin loading - recalculatePlayer(player, 40, false); + npc.getSkinTracker().updateViewer(player); } } }.runTaskLater(CitizensAPI.getPlugin(), delay); } - private List getNearbySkinnableNPCs(Player player) { + // hard reset skin update trackers for players near a skinnable NPC + private void resetNearbyPlayers(SkinnableEntity skinnable) { + Entity entity = skinnable.getBukkitEntity(); + if (entity == null || !entity.isValid()) + return; + double viewDistance = Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); + viewDistance *= viewDistance; + Location location = entity.getLocation(NPC_LOCATION); + List players = entity.getWorld().getPlayers(); + for (Player player : players) { + if (player.hasMetadata("NPC")) + continue; + + double distanceSquared = player.getLocation(CACHE_LOCATION).distanceSquared(location); + if (distanceSquared > viewDistance) + continue; + + SkinUpdateTracker tracker = skinUpdateTrackers.get(player.getUniqueId()); + if (tracker == null) { + tracker = new SkinUpdateTracker(player); + skinUpdateTrackers.put(player.getUniqueId(), tracker); + } + else { + tracker.hardReset(player); + } + } + } + + private List getNearbySkinnableNPCs(Player player) { List results = new ArrayList(); double viewDistance = Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); @@ -506,7 +547,7 @@ public class EventListen implements Listener { && player.canSee((Player) npcEntity) && player.getWorld().equals(npcEntity.getWorld()) && player.getLocation(CACHE_LOCATION) - .distanceSquared(npc.getStoredLocation()) < viewDistance) { + .distanceSquared(npc.getStoredLocation()) < viewDistance) { SkinnableEntity skinnable = NMS.getSkinnable(npcEntity); @@ -600,21 +641,25 @@ public class EventListen implements Listener { } private class SkinUpdateTracker { - float initialYaw; final Location location = new Location(null, 0, 0, 0); int rotationCount; + boolean hasMoved; float upperBound; float lowerBound; SkinUpdateTracker(Player player) { - reset(player); + hardReset(player); } boolean shouldUpdate(Player player) { + Location currentLoc = player.getLocation(CACHE_LOCATION); - Location currentLoc = player.getLocation(YAW_LOCATION); + if (!hasMoved) { + hasMoved = true; + return true; + } - if (rotationCount < 2) { + if (rotationCount < 3) { float yaw = NMS.clampYaw(currentLoc.getYaw()); boolean hasRotated = upperBound < lowerBound ? yaw > upperBound && yaw < lowerBound @@ -643,15 +688,23 @@ public class EventListen implements Listener { // resets initial yaw and location to the players // current location and yaw. void reset(Player player) { - player.getLocation(location); - this.initialYaw = NMS.clampYaw(location.getYaw()); - float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat(); - this.upperBound = NMS.clampYaw(this.initialYaw + rotationDegrees); - this.lowerBound = NMS.clampYaw(this.initialYaw - rotationDegrees); + player.getLocation(this.location); + if (rotationCount < 3) { + float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat(); + float yaw = NMS.clampYaw(this.location.getYaw()); + this.upperBound = NMS.clampYaw(yaw + rotationDegrees); + this.lowerBound = NMS.clampYaw(yaw - rotationDegrees); + } + } + + void hardReset(Player player) { + this.hasMoved = false; + this.rotationCount = 0; + reset(player); } } - private static final Location YAW_LOCATION = new Location(null, 0, 0, 0); private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0); + private static final Location NPC_LOCATION = new Location(null, 0, 0, 0); private static final int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50; } diff --git a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java index 1506e394b..3c4a08a3c 100644 --- a/src/main/java/net/citizensnpcs/npc/CitizensNPC.java +++ b/src/main/java/net/citizensnpcs/npc/CitizensNPC.java @@ -189,23 +189,11 @@ public class CitizensNPC extends AbstractNPC { boolean couldSpawn = !Util.isLoaded(at) ? false : mcEntity.world.addEntity(mcEntity, SpawnReason.CUSTOM); // send skin packets, if applicable, before other NMS packets are sent - SkinnableEntity skinnable = NMS.getSkinnable(getEntity()); - if (skinnable != null) { - final double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); - //skinnable.getSkinTracker().updateNearbyViewers(viewDistance); - Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { - @Override - public void run() { - if (getEntity() == null || !getEntity().isValid()) - return; - - SkinnableEntity npc = NMS.getSkinnable(getEntity()); - if (npc == null) - return; - - npc.getSkinTracker().updateNearbyViewers(viewDistance); - } - }, 20); + if (couldSpawn) { + SkinnableEntity skinnable = NMS.getSkinnable(getEntity()); + if (skinnable != null) { + skinnable.getSkinTracker().onSpawnNPC(); + } } mcEntity.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch()); diff --git a/src/main/java/net/citizensnpcs/npc/skin/Skin.java b/src/main/java/net/citizensnpcs/npc/skin/Skin.java index 85af82ca2..34cd58929 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/Skin.java +++ b/src/main/java/net/citizensnpcs/npc/skin/Skin.java @@ -182,7 +182,6 @@ public class Skin { } private void fetch() { - final int maxRetries = Setting.MAX_NPC_SKIN_RETRIES.asInt(); if (maxRetries > -1 && fetchRetries >= maxRetries) { if (Messaging.isDebugging()) { @@ -215,7 +214,7 @@ public class Skin { }, delay); if (Messaging.isDebugging()) { - Messaging.debug("Retrying skin fetch for '" + skinName + "' in " + delay + "ticks."); + Messaging.debug("Retrying skin fetch for '" + skinName + "' in " + delay + " ticks."); } break; case SUCCESS: @@ -227,6 +226,33 @@ public class Skin { }); } + /** + * Get a player skin. + * + *

+ * If a Skin instance does not exist, a new one is created and the skin data is automatically fetched. + *

+ * + * @param skinName + * The name of the skin. + */ + public static Skin get(String skinName) { + Preconditions.checkNotNull(skinName); + + skinName = skinName.toLowerCase(); + + Skin skin; + synchronized (CACHE) { + skin = CACHE.get(skinName); + } + + if (skin == null) { + skin = new Skin(skinName); + } + + return skin; + } + /** * Get a skin for a skinnable entity. * @@ -241,17 +267,7 @@ public class Skin { Preconditions.checkNotNull(entity); String skinName = entity.getSkinName().toLowerCase(); - - Skin skin; - synchronized (CACHE) { - skin = CACHE.get(skinName); - } - - if (skin == null) { - skin = new Skin(skinName); - } - - return skin; + return get(skinName); } /** @@ -272,7 +288,8 @@ public class Skin { private static void setNPCSkinData(SkinnableEntity entity, String skinName, UUID skinId, Property skinProperty) { NPC npc = entity.getNPC(); - // cache skins for faster initial skin availability + // cache skins for faster initial skin availability and + // for use when the latest skin is not required. npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName); npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString()); if (skinProperty.getValue() != null) { @@ -287,6 +304,16 @@ public class Skin { private static void setNPCTexture(SkinnableEntity entity, Property skinProperty) { GameProfile profile = entity.getProfile(); + + // don't set property if already set since this sometimes causes + // packet errors that disconnect the client. + Property current = Iterables.getFirst(profile.getProperties().get("textures"), null); + if (current != null + && current.getValue().equals(skinProperty.getValue()) + && current.getSignature().equals(skinProperty.getSignature())) { + return; + } + profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties. profile.getProperties().put("textures", skinProperty); } diff --git a/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java index 45b0426f2..59a01eb00 100644 --- a/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java +++ b/src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java @@ -5,12 +5,14 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import net.citizensnpcs.npc.CitizensNPC; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; import com.google.common.base.Preconditions; @@ -98,6 +100,23 @@ public class SkinPacketTracker { skin.applyAndRespawn(entity); } + /** + * Invoke when the NPC entity is spawned. + */ + public void onSpawnNPC() { + isRemoved = false; + new BukkitRunnable() { + @Override + public void run() { + if (!entity.getNPC().isSpawned()) + return; + + double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble(); + updateNearbyViewers(viewDistance); + } + }.runTaskLater(CitizensAPI.getPlugin(), 20); + } + /** * Invoke when the NPC entity is removed. * @@ -164,11 +183,11 @@ public class SkinPacketTracker { Player from = entity.getBukkitEntity(); Location location = from.getLocation(); - for (Player player : Bukkit.getServer().getOnlinePlayers()) { + for (Player player : world.getPlayers()) { if (player == null || player.hasMetadata("NPC")) continue; - if (world != player.getWorld() || !player.canSee(from)) + if (!player.canSee(from)) continue; if (location.distanceSquared(player.getLocation(CACHE_LOCATION)) > radius)