Merge pull request #490 from JCThePants/skins2

Improve player NPC skins
This commit is contained in:
fullwall 2015-08-27 21:13:59 +08:00
commit 7ff746a5b6
18 changed files with 1588 additions and 477 deletions

View File

@ -2,47 +2,12 @@ package net.citizensnpcs;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntityCombustByBlockEvent;
import org.bukkit.event.entity.EntityCombustByEntityEvent;
import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityTargetEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.vehicle.VehicleDestroyEvent;
import org.bukkit.event.vehicle.VehicleEnterEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
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.scoreboard.Team;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
@ -75,17 +40,54 @@ import net.citizensnpcs.api.trait.trait.Owner;
import net.citizensnpcs.api.util.DataKey; import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.editor.Editor; import net.citizensnpcs.editor.Editor;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.trait.Controllable; import net.citizensnpcs.trait.Controllable;
import net.citizensnpcs.trait.CurrentLocation; import net.citizensnpcs.trait.CurrentLocation;
import net.citizensnpcs.util.Messages; import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.NMS;
import net.minecraft.server.v1_8_R3.EntityPlayer;
import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntityCombustByBlockEvent;
import org.bukkit.event.entity.EntityCombustByEntityEvent;
import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityDamageByBlockEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityTargetEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.vehicle.VehicleDestroyEvent;
import org.bukkit.event.vehicle.VehicleEnterEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
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.scoreboard.Team;
public class EventListen implements Listener { public class EventListen implements Listener {
private final NPCRegistry npcRegistry = CitizensAPI.getNPCRegistry(); private final NPCRegistry npcRegistry = CitizensAPI.getNPCRegistry();
private final Map<String, NPCRegistry> registries; private final Map<String, NPCRegistry> registries;
private final ListMultimap<ChunkCoord, NPC> toRespawn = ArrayListMultimap.create(); private final ListMultimap<ChunkCoord, NPC> toRespawn = ArrayListMultimap.create();
private final Map<UUID, SkinUpdateTracker> skinUpdateTrackers =
new HashMap<UUID, SkinUpdateTracker>(Bukkit.getMaxPlayers() / 2);
EventListen(Map<String, NPCRegistry> registries) { EventListen(Map<String, NPCRegistry> registries) {
this.registries = registries; this.registries = registries;
@ -339,7 +341,7 @@ public class EventListen implements Listener {
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangeWorld(PlayerChangedWorldEvent event) { public void onPlayerChangeWorld(PlayerChangedWorldEvent event) {
recalculatePlayer(event.getPlayer()); recalculatePlayer(event.getPlayer(), 20, true);
} }
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
@ -361,7 +363,7 @@ public class EventListen implements Listener {
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
recalculatePlayer(event.getPlayer()); recalculatePlayer(event.getPlayer(), 20, true);
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
@ -373,16 +375,17 @@ public class EventListen implements Listener {
event.getPlayer().leaveVehicle(); event.getPlayer().leaveVehicle();
} }
} }
skinUpdateTrackers.remove(event.getPlayer().getUniqueId());
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) { public void onPlayerRespawn(PlayerRespawnEvent event) {
recalculatePlayer(event.getPlayer()); recalculatePlayer(event.getPlayer(), 15, true);
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerTeleport(PlayerTeleportEvent event) { public void onPlayerTeleport(PlayerTeleportEvent event) {
recalculatePlayer(event.getPlayer()); recalculatePlayer(event.getPlayer(), 15, true);
} }
@EventHandler @EventHandler
@ -433,34 +436,71 @@ public class EventListen implements Listener {
} }
} }
public void recalculatePlayer(final Player player) { // recalculate player NPCs the first time a player moves and every time
// 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;
if (!updateTracker.shouldUpdate(event.getPlayer()))
return;
recalculatePlayer(event.getPlayer(), 10, false);
}
public void recalculatePlayer(final Player player, long delay, final boolean isInitial) {
if (player.hasMetadata("NPC"))
return;
if (isInitial) {
skinUpdateTrackers.put(player.getUniqueId(), new SkinUpdateTracker(player));
}
new BukkitRunnable() { new BukkitRunnable() {
@Override @Override
public void run() { public void run() {
final List<EntityPlayer> nearbyNPCs = new ArrayList<EntityPlayer>();
for (NPC npc : getAllNPCs()) {
Entity npcEntity = npc.getEntity();
if (npcEntity instanceof Player && player.canSee((Player) npcEntity)
&& player.getWorld().equals(npcEntity.getWorld())
&& player.getLocation().distanceSquared(npcEntity.getLocation()) < 100 * 100) {
nearbyNPCs.add(((CraftPlayer) npcEntity).getHandle());
}
}
new BukkitRunnable() {
@Override
public void run() {
sendToPlayer(player, nearbyNPCs);
}
}.runTaskLater(CitizensAPI.getPlugin(), 30);
new BukkitRunnable() {
@Override
public void run() {
sendToPlayer(player, nearbyNPCs);
}
}.runTaskLater(CitizensAPI.getPlugin(), 70);
}
}.runTaskLater(CitizensAPI.getPlugin(), 10);
List<SkinnableEntity> 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);
}
}
}.runTaskLater(CitizensAPI.getPlugin(), delay);
}
private List<SkinnableEntity> getNearbySkinnableNPCs(Player player) {
List<SkinnableEntity> results = new ArrayList<SkinnableEntity>();
double viewDistance = Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
viewDistance *= viewDistance;
for (NPC npc : getAllNPCs()) {
Entity npcEntity = npc.getEntity();
if (npcEntity instanceof Player
&& player.canSee((Player) npcEntity)
&& player.getWorld().equals(npcEntity.getWorld())
&& player.getLocation(CACHE_LOCATION)
.distanceSquared(npc.getStoredLocation()) < viewDistance) {
SkinnableEntity skinnable = NMS.getSkinnable(npcEntity);
results.add(skinnable);
}
}
return results;
} }
private void respawnAllFromCoord(ChunkCoord coord) { private void respawnAllFromCoord(ChunkCoord coord) {
@ -482,30 +522,6 @@ public class EventListen implements Listener {
} }
} }
void sendToPlayer(final Player player, final List<EntityPlayer> nearbyNPCs) {
if (!player.isValid())
return;
for (EntityPlayer nearbyNPC : nearbyNPCs) {
if (nearbyNPC.isAlive())
NMS.sendPacket(player, new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, nearbyNPC));
}
if (Setting.DISABLE_TABLIST.asBoolean()) {
new BukkitRunnable() {
@Override
public void run() {
if (!player.isValid())
return;
for (EntityPlayer nearbyNPC : nearbyNPCs) {
if (nearbyNPC.isAlive())
NMS.sendPacket(player, new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, nearbyNPC));
}
}
}.runTaskLater(CitizensAPI.getPlugin(), 2);
}
}
private boolean spawn(NPC npc) { private boolean spawn(NPC npc) {
Location spawn = npc.getTrait(CurrentLocation.class).getLocation(); Location spawn = npc.getTrait(CurrentLocation.class).getLocation();
if (spawn == null) { if (spawn == null) {
@ -569,4 +585,65 @@ public class EventListen implements Listener {
return prime * (prime * (prime + ((worldName == null) ? 0 : worldName.hashCode())) + x) + z; return prime * (prime * (prime + ((worldName == null) ? 0 : worldName.hashCode())) + x) + z;
} }
} }
private class SkinUpdateTracker {
float initialYaw;
final Location location = new Location(null, 0, 0, 0);
boolean hasMoved;
int rotationCount;
SkinUpdateTracker(Player player) {
reset(player);
}
boolean shouldUpdate(Player player) {
// check if this is the first time the player has moved
if (!hasMoved) {
hasMoved = true;
reset(player);
return true;
}
Location currentLoc = player.getLocation(YAW_LOCATION);
float currentYaw = currentLoc.getYaw();
if (rotationCount < 2) {
float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat();
boolean hasRotated =
Math.abs(NMS.clampYaw(currentYaw - this.initialYaw)) < rotationDegrees;
// update the first 2 times the player rotates. helps load skins around player
// after the player logs/teleports.
if (hasRotated) {
rotationCount++;
reset(player);
return true;
}
}
// update every time a player moves a certain distance
double distance = currentLoc.distanceSquared(this.location);
if (distance > MOVEMENT_SKIN_UPDATE_DISTANCE) {
reset(player);
return true;
}
else {
return false;
}
}
// resets initial yaw and location to the players
// current location and yaw.
void reset(Player player) {
player.getLocation(location);
this.initialYaw = location.getYaw();
}
}
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 int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50;
} }

View File

@ -88,14 +88,16 @@ public class Settings {
KEEP_CHUNKS_LOADED("npc.chunks.always-keep-loaded", false), KEEP_CHUNKS_LOADED("npc.chunks.always-keep-loaded", false),
LOCALE("general.translation.locale", ""), LOCALE("general.translation.locale", ""),
MAX_NPC_LIMIT_CHECKS("npc.limits.max-permission-checks", 100), MAX_NPC_LIMIT_CHECKS("npc.limits.max-permission-checks", 100),
MAX_NPC_SKIN_RETRIES("npc.skins.max-retries", -1), MAX_PACKET_ENTRIES("npc.limits.max-packet-entries", 15),
MAX_SPEED("npc.limits.max-speed", 100), MAX_SPEED("npc.limits.max-speed", 100),
MAX_TEXT_RANGE("npc.chat.options.max-text-range", 500), MAX_TEXT_RANGE("npc.chat.options.max-text-range", 500),
MESSAGE_COLOUR("general.color-scheme.message", "<a>"), MESSAGE_COLOUR("general.color-scheme.message", "<a>"),
NEW_PATHFINDER_OPENS_DOORS("npc.pathfinding.new-finder-open-doors", false), NEW_PATHFINDER_OPENS_DOORS("npc.pathfinding.new-finder-open-doors", false),
NPC_ATTACK_DISTANCE("npc.pathfinding.attack-range", 1.75 * 1.75), NPC_ATTACK_DISTANCE("npc.pathfinding.attack-range", 1.75 * 1.75),
NPC_COST("economy.npc.cost", 100D), NPC_COST("economy.npc.cost", 100D),
NPC_SKIN_RETRY_DELAY("npc.skins.retry-delay", 120), NPC_SKIN_UPDATE("npc.skins.update", false),
NPC_SKIN_VIEW_DISTANCE("npc.skins.view-distance", 100D),
NPC_SKIN_ROTATION_UPDATE_DEGREES("npc.skins.rotation-update-degrees", 90f),
PACKET_UPDATE_DELAY("npc.packets.update-delay", 30), PACKET_UPDATE_DELAY("npc.packets.update-delay", 30),
QUICK_SELECT("npc.selection.quick-select", false), QUICK_SELECT("npc.selection.quick-select", false),
REMOVE_PLAYERS_FROM_PLAYER_LIST("npc.player.remove-from-list", true), REMOVE_PLAYERS_FROM_PLAYER_LIST("npc.player.remove-from-list", true),

View File

@ -5,6 +5,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.DyeColor; import org.bukkit.DyeColor;
@ -987,11 +988,13 @@ public class NPCCommands {
boolean remove = !npc.data().get("removefromplayerlist", Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean()); boolean remove = !npc.data().get("removefromplayerlist", Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean());
if (args.hasFlag('a')) { if (args.hasFlag('a')) {
remove = false; remove = false;
} else if (args.hasFlag('r')) } else if (args.hasFlag('r')) {
remove = true; remove = true;
}
npc.data().setPersistent("removefromplayerlist", remove); npc.data().setPersistent("removefromplayerlist", remove);
if (npc.isSpawned()) { if (npc.isSpawned()) {
NMS.addOrRemoveFromPlayerList(npc.getEntity(), remove); npc.despawn(DespawnReason.PENDING_RESPAWN);
npc.spawn(npc.getTrait(CurrentLocation.class).getLocation());
} }
Messaging.sendTr(sender, remove ? Messages.REMOVED_FROM_PLAYERLIST : Messages.ADDED_TO_PLAYERLIST, Messaging.sendTr(sender, remove ? Messages.REMOVED_FROM_PLAYERLIST : Messages.ADDED_TO_PLAYERLIST,
npc.getName()); npc.getName());
@ -1310,8 +1313,11 @@ public class NPCCommands {
} }
Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName); Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName);
if (npc.isSpawned()) { if (npc.isSpawned()) {
npc.despawn(DespawnReason.PENDING_RESPAWN);
npc.spawn(npc.getStoredLocation()); SkinnableEntity skinnable = NMS.getSkinnable(npc.getEntity());
if (skinnable != null) {
skinnable.setSkinName(skinName);
}
} }
} }

View File

@ -2,7 +2,6 @@ package net.citizensnpcs.npc;
import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.NMS;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;

View File

@ -1,28 +1,13 @@
package net.citizensnpcs.npc; package net.citizensnpcs.npc;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.UUID; import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftLivingEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scoreboard.NameTagVisibility;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import net.citizensnpcs.NPCNeedsRespawnEvent; import net.citizensnpcs.NPCNeedsRespawnEvent;
import net.citizensnpcs.Settings;
import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.Navigator; import net.citizensnpcs.api.ai.Navigator;
@ -41,13 +26,26 @@ import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.npc.ai.CitizensBlockBreaker; import net.citizensnpcs.npc.ai.CitizensBlockBreaker;
import net.citizensnpcs.npc.ai.CitizensNavigator; import net.citizensnpcs.npc.ai.CitizensNavigator;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.trait.CurrentLocation; import net.citizensnpcs.trait.CurrentLocation;
import net.citizensnpcs.util.Messages; import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.NMS;
import net.citizensnpcs.util.Util; import net.citizensnpcs.util.Util;
import net.minecraft.server.v1_8_R3.Packet;
import net.minecraft.server.v1_8_R3.PacketPlayOutEntityTeleport; import net.minecraft.server.v1_8_R3.PacketPlayOutEntityTeleport;
import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftLivingEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.scoreboard.NameTagVisibility;
public class CitizensNPC extends AbstractNPC { public class CitizensNPC extends AbstractNPC {
private EntityController entityController; private EntityController entityController;
@ -190,6 +188,27 @@ public class CitizensNPC extends AbstractNPC {
net.minecraft.server.v1_8_R3.Entity mcEntity = ((CraftEntity) getEntity()).getHandle(); net.minecraft.server.v1_8_R3.Entity mcEntity = ((CraftEntity) getEntity()).getHandle();
boolean couldSpawn = !Util.isLoaded(at) ? false : mcEntity.world.addEntity(mcEntity, SpawnReason.CUSTOM); 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);
}
mcEntity.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch()); mcEntity.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch());
if (!couldSpawn) { if (!couldSpawn) {
@ -244,29 +263,9 @@ public class CitizensNPC extends AbstractNPC {
if (getEntity() instanceof Player) { if (getEntity() instanceof Player) {
final CraftPlayer player = (CraftPlayer) getEntity(); final CraftPlayer player = (CraftPlayer) getEntity();
NMS.replaceTrackerEntry(player); NMS.replaceTrackerEntry(player);
new BukkitRunnable() {
@Override
public void run() {
NMS.sendPacketsNearby(player, player.getLocation(),
Arrays.asList((Packet) new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, player.getHandle())),
200.0);
if (Setting.DISABLE_TABLIST.asBoolean()) {
new BukkitRunnable() {
@Override
public void run() {
NMS.sendPacketsNearby(player, player.getLocation(),
Arrays.asList((Packet) new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER,
player.getHandle())),
200.0);
}
}.runTaskLater(CitizensAPI.getPlugin(), 2);
}
}
}.runTaskLater(CitizensAPI.getPlugin(), 2);
} }
} }
return true; return true;
} }

View File

@ -4,9 +4,13 @@ import java.io.IOException;
import java.net.Socket; import java.net.Socket;
import java.util.List; import java.util.List;
import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.event.NPCPushEvent; import net.citizensnpcs.api.event.NPCPushEvent;
import net.citizensnpcs.api.npc.MetadataStore;
import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.trait.trait.Inventory; import net.citizensnpcs.api.trait.trait.Inventory;
import net.citizensnpcs.npc.CitizensNPC; import net.citizensnpcs.npc.CitizensNPC;
@ -14,6 +18,8 @@ import net.citizensnpcs.npc.ai.NPCHolder;
import net.citizensnpcs.npc.network.EmptyNetHandler; import net.citizensnpcs.npc.network.EmptyNetHandler;
import net.citizensnpcs.npc.network.EmptyNetworkManager; import net.citizensnpcs.npc.network.EmptyNetworkManager;
import net.citizensnpcs.npc.network.EmptySocket; import net.citizensnpcs.npc.network.EmptySocket;
import net.citizensnpcs.npc.skin.SkinPacketTracker;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.NMS;
import net.citizensnpcs.util.Util; import net.citizensnpcs.util.Util;
import net.citizensnpcs.util.nms.PlayerControllerJump; import net.citizensnpcs.util.nms.PlayerControllerJump;
@ -40,16 +46,16 @@ import net.minecraft.server.v1_8_R3.WorldServer;
import net.minecraft.server.v1_8_R3.WorldSettings.EnumGamemode; import net.minecraft.server.v1_8_R3.WorldSettings.EnumGamemode;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_8_R3.CraftServer; import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.bukkit.metadata.MetadataValue; import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import com.mojang.authlib.GameProfile; public class EntityHumanNPC extends EntityPlayer implements NPCHolder, SkinnableEntity {
public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
private PlayerControllerJump controllerJump; private PlayerControllerJump controllerJump;
private PlayerControllerLook controllerLook; private PlayerControllerLook controllerLook;
private PlayerControllerMove controllerMove; private PlayerControllerMove controllerMove;
@ -58,16 +64,21 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
private PlayerNavigation navigation; private PlayerNavigation navigation;
private final CitizensNPC npc; private final CitizensNPC npc;
private final Location packetLocationCache = new Location(null, 0, 0, 0); private final Location packetLocationCache = new Location(null, 0, 0, 0);
private final SkinPacketTracker skinTracker;
public EntityHumanNPC(MinecraftServer minecraftServer, WorldServer world, GameProfile gameProfile, public EntityHumanNPC(MinecraftServer minecraftServer, WorldServer world, GameProfile gameProfile,
PlayerInteractManager playerInteractManager, NPC npc) { PlayerInteractManager playerInteractManager, NPC npc) {
super(minecraftServer, world, gameProfile, playerInteractManager); super(minecraftServer, world, gameProfile, playerInteractManager);
this.npc = (CitizensNPC) npc; this.npc = (CitizensNPC) npc;
if (npc != null) { if (npc != null) {
skinTracker = new SkinPacketTracker(this);
playerInteractManager.setGameMode(EnumGamemode.SURVIVAL); playerInteractManager.setGameMode(EnumGamemode.SURVIVAL);
initialise(minecraftServer); initialise(minecraftServer);
} }
else {
skinTracker = null;
}
} }
@Override @Override
@ -185,6 +196,31 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
return npc; return npc;
} }
@Override
public SkinPacketTracker getSkinTracker() {
return skinTracker;
}
@Override
public String getSkinName() {
MetadataStore meta = npc.data();
String skinName = meta.get(NPC.PLAYER_SKIN_UUID_METADATA);
if (skinName == null) {
skinName = ChatColor.stripColor(getName());
}
return skinName.toLowerCase();
}
@Override
public void setSkinName(String name) {
Preconditions.checkNotNull(name);
npc.data().setPersistent(NPC.PLAYER_SKIN_UUID_METADATA, name.toLowerCase());
skinTracker.notifySkinChange();
}
private void initialise(MinecraftServer minecraftServer) { private void initialise(MinecraftServer minecraftServer) {
Socket socket = new EmptySocket(); Socket socket = new EmptySocket();
NetworkManager conn = null; NetworkManager conn = null;
@ -296,6 +332,7 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
} }
private void updatePackets(boolean navigating) { private void updatePackets(boolean navigating) {
if (world.getWorld().getFullTime() % Setting.PACKET_UPDATE_DELAY.asInt() == 0) { if (world.getWorld().getFullTime() % Setting.PACKET_UPDATE_DELAY.asInt() == 0) {
// set skin flag byte to all visible (DataWatcher API is lacking so // set skin flag byte to all visible (DataWatcher API is lacking so
// catch the NPE as a sign that this is a MC 1.7 server without the // catch the NPE as a sign that this is a MC 1.7 server without the
@ -316,10 +353,6 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
packets[i] = new PacketPlayOutEntityEquipment(getId(), i, getEquipment(i)); packets[i] = new PacketPlayOutEntityEquipment(getId(), i, getEquipment(i));
} }
boolean removeFromPlayerList = npc.data().get("removefromplayerlist",
Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean());
NMS.addOrRemoveFromPlayerList(getBukkitEntity(), removeFromPlayerList);
NMS.sendPlayerlistPacket(false, getBukkitEntity());
NMS.sendPacketsNearby(getBukkitEntity(), current, packets); NMS.sendPacketsNearby(getBukkitEntity(), current, packets);
} }
} }
@ -328,7 +361,7 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
this.navigation.setRange(pathfindingRange); this.navigation.setRange(pathfindingRange);
} }
public static class PlayerNPC extends CraftPlayer implements NPCHolder { public static class PlayerNPC extends CraftPlayer implements NPCHolder, SkinnableEntity {
private final CraftServer cserver; private final CraftServer cserver;
private final CitizensNPC npc; private final CitizensNPC npc;
@ -372,6 +405,26 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
cserver.getEntityMetadata().setMetadata(this, metadataKey, newMetadataValue); cserver.getEntityMetadata().setMetadata(this, metadataKey, newMetadataValue);
} }
@Override
public SkinPacketTracker getSkinTracker() {
return ((SkinnableEntity)this.entity).getSkinTracker();
}
@Override
public Player getBukkitEntity() {
return this;
}
@Override
public String getSkinName() {
return ((SkinnableEntity)this.entity).getSkinName();
}
@Override
public void setSkinName(String name) {
((SkinnableEntity)this.entity).setSkinName(name);
}
} }
private static final float EPSILON = 0.005F; private static final float EPSILON = 0.005F;

View File

@ -1,57 +1,32 @@
package net.citizensnpcs.npc.entity; package net.citizensnpcs.npc.entity;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.Colorizer;
import net.citizensnpcs.npc.AbstractEntityController;
import net.citizensnpcs.npc.skin.Skin;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.util.NMS;
import net.minecraft.server.v1_8_R3.PlayerInteractManager;
import net.minecraft.server.v1_8_R3.WorldServer;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.scoreboard.Scoreboard; import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team; import org.bukkit.scoreboard.Team;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.mojang.authlib.Agent;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.GameProfileRepository;
import com.mojang.authlib.HttpAuthenticationService;
import com.mojang.authlib.ProfileLookupCallback;
import com.mojang.authlib.minecraft.MinecraftSessionService;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse;
import com.mojang.util.UUIDTypeAdapter;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.event.DespawnReason;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.Colorizer;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.npc.AbstractEntityController;
import net.citizensnpcs.util.NMS;
import net.minecraft.server.v1_8_R3.PlayerInteractManager;
import net.minecraft.server.v1_8_R3.WorldServer;
public class HumanController extends AbstractEntityController { public class HumanController extends AbstractEntityController {
public HumanController() { public HumanController() {
super(); super();
if (SKIN_THREAD == null) {
Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), SKIN_THREAD = new SkinThread(),
10, 10);
}
} }
@Override @Override
@ -100,20 +75,26 @@ public class HumanController extends AbstractEntityController {
msb |= 0x0000000000002000L; msb |= 0x0000000000002000L;
uuid = new UUID(msb, uuid.getLeastSignificantBits()); uuid = new UUID(msb, uuid.getLeastSignificantBits());
} }
GameProfile profile = new GameProfile(uuid, coloredName); GameProfile profile = new GameProfile(uuid, coloredName);
updateSkin(npc, nmsWorld, profile);
final EntityHumanNPC handle = new EntityHumanNPC(nmsWorld.getServer().getServer(), nmsWorld, profile, final EntityHumanNPC handle = new EntityHumanNPC(nmsWorld.getServer().getServer(), nmsWorld, profile,
new PlayerInteractManager(nmsWorld), npc); new PlayerInteractManager(nmsWorld), npc);
Skin skin = handle.getSkinTracker().getSkin();
if (skin != null) {
skin.apply(handle);
}
handle.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch()); handle.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch());
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override @Override
public void run() { public void run() {
if (getBukkitEntity() == null || !getBukkitEntity().isValid()) if (getBukkitEntity() == null || !getBukkitEntity().isValid())
return; return;
boolean removeFromPlayerList = Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean();
NMS.addOrRemoveFromPlayerList(getBukkitEntity(),
npc.data().get("removefromplayerlist", removeFromPlayerList));
if (prefixCapture != null) { if (prefixCapture != null) {
Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard(); Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
@ -133,6 +114,7 @@ public class HumanController extends AbstractEntityController {
} }
} }
}, 1); }, 1);
handle.getBukkitEntity().setSleepingIgnored(true); handle.getBukkitEntity().setSleepingIgnored(true);
return handle.getBukkitEntity(); return handle.getBukkitEntity();
@ -145,235 +127,14 @@ public class HumanController extends AbstractEntityController {
@Override @Override
public void remove() { public void remove() {
NMS.sendPlayerlistPacket(false, getBukkitEntity());
NMS.removeFromWorld(getBukkitEntity());
SkinnableEntity npc = NMS.getSkinnable(getBukkitEntity());
npc.getSkinTracker().onRemoveNPC();
super.remove(); super.remove();
} }
private void updateSkin(final NPC npc, final WorldServer nmsWorld, GameProfile profile) {
String skinUUID = npc.data().get(NPC.PLAYER_SKIN_UUID_METADATA);
if (skinUUID == null) {
skinUUID = npc.getName();
}
if (npc.data().has(CACHED_SKIN_UUID_METADATA) && npc.data().has(CACHED_SKIN_UUID_NAME_METADATA)
&& ChatColor.stripColor(skinUUID).equalsIgnoreCase(
ChatColor.stripColor(npc.data().<String> get(CACHED_SKIN_UUID_NAME_METADATA)))) {
skinUUID = npc.data().get(CACHED_SKIN_UUID_METADATA);
}
if (npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES)
&& npc.data().<String> get(PLAYER_SKIN_TEXTURE_PROPERTIES).equals("cache")) {
SKIN_THREAD.addRunnable(
new SkinFetcher(new UUIDFetcher(skinUUID, npc), nmsWorld.getMinecraftServer().aD(), npc));
return;
}
Property cached = TEXTURE_CACHE.get(skinUUID);
if (npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES) && npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN)) {
cached = new Property("textures", npc.data().<String> get(PLAYER_SKIN_TEXTURE_PROPERTIES),
npc.data().<String> get(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN));
}
if (cached != null) {
profile.getProperties().put("textures", cached);
} else {
SKIN_THREAD.addRunnable(
new SkinFetcher(new UUIDFetcher(skinUUID, npc), nmsWorld.getMinecraftServer().aD(), npc));
}
}
private static class SkinFetcher implements Runnable {
private final NPC npc;
private final MinecraftSessionService repo;
private final Callable<String> uuid;
public SkinFetcher(Callable<String> uuid, MinecraftSessionService repo, NPC npc) {
this.uuid = uuid;
this.repo = repo;
this.npc = npc;
}
/*
* Yggdrasil's default implementation of this method silently fails instead of throwing an Exception like it should.
*/
private GameProfile fillProfileProperties(YggdrasilAuthenticationService auth, GameProfile profile,
boolean requireSecure) throws Exception {
URL url = HttpAuthenticationService.constantURL(
new StringBuilder().append("https://sessionserver.mojang.com/session/minecraft/profile/")
.append(UUIDTypeAdapter.fromUUID(profile.getId())).toString());
url = HttpAuthenticationService.concatenateURL(url,
new StringBuilder().append("unsigned=").append(!requireSecure).toString());
MinecraftProfilePropertiesResponse response = (MinecraftProfilePropertiesResponse) MAKE_REQUEST.invoke(auth,
url, null, MinecraftProfilePropertiesResponse.class);
if (response == null) {
return profile;
}
GameProfile result = new GameProfile(response.getId(), response.getName());
result.getProperties().putAll(response.getProperties());
profile.getProperties().putAll(response.getProperties());
return result;
}
@Override
public void run() {
String realUUID;
try {
realUUID = uuid.call();
} catch (Exception e) {
return;
}
GameProfile skinProfile = null;
Property cached = TEXTURE_CACHE.get(realUUID);
if (cached != null && !(npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES)
&& npc.data().<String> get(PLAYER_SKIN_TEXTURE_PROPERTIES).equals("cache"))) {
if (Messaging.isDebugging()) {
Messaging
.debug("Using cached skin texture for NPC " + npc.getName() + " UUID " + npc.getUniqueId());
}
skinProfile = new GameProfile(UUID.fromString(realUUID), "");
skinProfile.getProperties().put("textures", cached);
} else {
try {
skinProfile = fillProfileProperties(
((YggdrasilMinecraftSessionService) repo).getAuthenticationService(),
new GameProfile(UUID.fromString(realUUID), ""), true);
} catch (Exception e) {
if ((e.getMessage() != null && e.getMessage().contains("too many requests"))
|| (e.getCause() != null && e.getCause().getMessage() != null
&& e.getCause().getMessage().contains("too many requests"))) {
SKIN_THREAD.delay();
SKIN_THREAD.addRunnable(this);
}
return;
}
if (skinProfile == null || !skinProfile.getProperties().containsKey("textures"))
return;
Property textures = Iterables.getFirst(skinProfile.getProperties().get("textures"), null);
if (textures.getValue() == null || textures.getSignature() == null)
return;
if (npc.data().has(PLAYER_SKIN_TEXTURE_PROPERTIES)
&& npc.data().<String> get(PLAYER_SKIN_TEXTURE_PROPERTIES).equals("cache")) {
npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES, textures.getValue());
npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN, textures.getSignature());
}
if (Messaging.isDebugging()) {
Messaging.debug("Fetched skin texture for UUID " + realUUID + " for NPC " + npc.getName() + " UUID "
+ npc.getUniqueId());
}
TEXTURE_CACHE.put(realUUID, new Property("textures", textures.getValue(), textures.getSignature()));
}
if (CitizensAPI.getPlugin().isEnabled()) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
if (npc.isSpawned()) {
npc.despawn(DespawnReason.PENDING_RESPAWN);
npc.spawn(npc.getStoredLocation());
}
}
});
}
}
}
public static class SkinThread implements Runnable {
private volatile int delay = 0;
private volatile int retryTimes = 0;
private final BlockingDeque<Runnable> tasks = new LinkedBlockingDeque<Runnable>();
public void addRunnable(Runnable r) {
Iterator<Runnable> itr = tasks.iterator();
while (itr.hasNext()) {
if (((SkinFetcher) itr.next()).npc.getUniqueId().equals(((SkinFetcher) r).npc.getUniqueId())) {
itr.remove();
}
}
tasks.offer(r);
}
public void delay() {
delay = Setting.NPC_SKIN_RETRY_DELAY.asInt();
// need to wait before Mojang accepts API calls again
retryTimes++;
if (Setting.MAX_NPC_SKIN_RETRIES.asInt() >= 0 && retryTimes > Setting.MAX_NPC_SKIN_RETRIES.asInt()) {
tasks.clear();
retryTimes = 0;
}
}
@Override
public void run() {
if (delay > 0) {
delay--;
return;
}
Runnable r = tasks.pollFirst();
if (r == null) {
return;
}
r.run();
}
}
public static class UUIDFetcher implements Callable<String> {
private final NPC npc;
private String reportedUUID;
public UUIDFetcher(String reportedUUID, NPC npc) {
this.reportedUUID = reportedUUID;
this.npc = npc;
}
@Override
public String call() throws Exception {
String skinUUID = UUID_CACHE.get(reportedUUID);
if (skinUUID != null) {
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinUUID);
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, reportedUUID);
reportedUUID = skinUUID;
}
if (reportedUUID.contains("-")) {
return reportedUUID;
}
final GameProfileRepository repo = ((CraftServer) Bukkit.getServer()).getServer()
.getGameProfileRepository();
repo.findProfilesByNames(new String[] { ChatColor.stripColor(reportedUUID) }, Agent.MINECRAFT,
new ProfileLookupCallback() {
@Override
public void onProfileLookupFailed(GameProfile arg0, Exception arg1) {
}
@Override
public void onProfileLookupSucceeded(final GameProfile profile) {
UUID_CACHE.put(reportedUUID, profile.getId().toString());
if (Messaging.isDebugging()) {
Messaging.debug("Fetched UUID " + profile.getId() + " for NPC " + npc.getName()
+ " UUID " + npc.getUniqueId());
}
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, profile.getId().toString());
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, profile.getName());
}
});
return npc.data().get(CACHED_SKIN_UUID_METADATA, reportedUUID);
}
}
private static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
private static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
private static Method MAKE_REQUEST;
private static Pattern NON_ALPHABET_MATCHER = Pattern.compile(".*[^A-Za-z0-9_].*"); private static Pattern NON_ALPHABET_MATCHER = Pattern.compile(".*[^A-Za-z0-9_].*");
private static final String PLAYER_SKIN_TEXTURE_PROPERTIES = "player-skin-textures";
private static final String PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN = "player-skin-signature";
private static SkinThread SKIN_THREAD;
private static final Map<String, Property> TEXTURE_CACHE = Maps.newConcurrentMap();
private static final Map<String, String> UUID_CACHE = Maps.newConcurrentMap();
static {
try {
MAKE_REQUEST = YggdrasilAuthenticationService.class.getDeclaredMethod("makeRequest", URL.class,
Object.class, Class.class);
MAKE_REQUEST.setAccessible(true);
} catch (Exception ex) {
ex.printStackTrace();
}
}
} }

View File

@ -0,0 +1,14 @@
package net.citizensnpcs.npc.profile;
/**
* Interface for a subscriber of the results of a profile fetch.
*/
public interface ProfileFetchHandler {
/**
* Invoked when a result for a profile is ready.
*
* @param request The profile request that was handled.
*/
void onResult(ProfileRequest request);
}

View File

@ -0,0 +1,29 @@
package net.citizensnpcs.npc.profile;
/**
* The result status of a profile fetch.
*/
public enum ProfileFetchResult {
/**
* The profile has not been fetched yet.
*/
PENDING,
/**
* The profile was successfully fetched.
*/
SUCCESS,
/**
* The profile request failed for unknown reasons.
*/
FAILED,
/**
* The profile request failed because the profile
* was not found.
*/
NOT_FOUND,
/**
* The profile request failed because too many requests
* were sent.
*/
TOO_MANY_REQUESTS
}

View File

@ -0,0 +1,115 @@
package net.citizensnpcs.npc.profile;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import net.citizensnpcs.api.CitizensAPI;
import org.bukkit.Bukkit;
/**
* Thread used to fetch profiles from the Mojang servers.
*
* <p>Maintains a cache of profiles so that no profile is ever requested more than once
* during a single server session.</p>
*
* @see ProfileFetcher
*/
class ProfileFetchThread implements Runnable {
private final ProfileFetcher profileFetcher = new ProfileFetcher();
private final Deque<ProfileRequest> queue = new ArrayDeque<ProfileRequest>();
private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(35);
private final Object sync = new Object(); // sync for queue & requested fields
ProfileFetchThread() {}
/**
* Fetch a profile.
*
* @param name The name of the player the profile belongs to.
* @param handler Optional handler to handle result fetch result.
* Handler always invoked from the main thread.
*
* @see ProfileFetcher#fetch
*/
void fetch(String name, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(name);
name = name.toLowerCase();
ProfileRequest request;
synchronized (sync) {
request = requested.get(name);
if (request == null) {
request = new ProfileRequest(name, handler);
queue.add(request);
requested.put(name, request);
return;
}
}
if (handler != null) {
if (request.getResult() == ProfileFetchResult.PENDING) {
addHandler(request, handler);
}
else {
sendResult(handler, request);
}
}
}
@Override
public void run() {
List<ProfileRequest> requests;
synchronized (sync) {
if (queue.isEmpty())
return;
requests = new ArrayList<ProfileRequest>(queue);
queue.clear();
}
profileFetcher.fetchRequests(requests);
}
private static void sendResult(final ProfileFetchHandler handler,
final ProfileRequest request) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
new Runnable() {
@Override
public void run() {
handler.onResult(request);
}
}, 1);
}
private static void addHandler(final ProfileRequest request,
final ProfileFetchHandler handler) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(),
new Runnable() {
@Override
public void run() {
request.addHandler(handler);
}
}, 1);
}
}

View File

@ -0,0 +1,154 @@
package net.citizensnpcs.npc.profile;
import java.util.Collection;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.mojang.authlib.Agent;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.GameProfileRepository;
import com.mojang.authlib.ProfileLookupCallback;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.NMS;
import org.bukkit.Bukkit;
/**
* Fetches game profiles that include skin data from Mojang servers.
*
* @see ProfileFetchThread
*/
public class ProfileFetcher {
/**
* Fetch a profile.
*
* @param name The name of the player the profile belongs to.
* @param handler Optional handler to handle the result.
* Handler always invoked from the main thread.
*/
public static void fetch(String name, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(name);
if (PROFILE_THREAD == null) {
PROFILE_THREAD = new ProfileFetchThread();
Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD,
11, 20);
}
PROFILE_THREAD.fetch(name, handler);
}
ProfileFetcher() {}
/**
* Fetch one or more profiles.
*
* @param requests The profile requests.
*/
void fetchRequests(final Collection<ProfileRequest> requests) {
Preconditions.checkNotNull(requests);
final GameProfileRepository repo = NMS.getGameProfileRepository();
String[] playerNames = new String[requests.size()];
int i=0;
for (ProfileRequest request : requests) {
playerNames[i] = request.getPlayerName();
i++;
}
repo.findProfilesByNames(playerNames, Agent.MINECRAFT,
new ProfileLookupCallback() {
@Override
public void onProfileLookupFailed(GameProfile profile, Exception e) {
if (Messaging.isDebugging()) {
Messaging.debug("Profile lookup for player '" +
profile.getName() + "' failed: " + getExceptionMsg(e));
}
ProfileRequest request = findRequest(profile.getName(), requests);
if (request == null)
return;
if (isProfileNotFound(e)) {
request.setResult(null, ProfileFetchResult.NOT_FOUND);
} else if (isTooManyRequests(e)) {
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
} else {
request.setResult(null, ProfileFetchResult.FAILED);
}
}
@Override
public void onProfileLookupSucceeded(final GameProfile profile) {
if (Messaging.isDebugging()) {
Messaging.debug("Fetched profile " + profile.getId()
+ " for player " + profile.getName());
}
ProfileRequest request = findRequest(profile.getName(), requests);
if (request == null)
return;
try {
request.setResult(NMS.fillProfileProperties(profile, true), ProfileFetchResult.SUCCESS);
} catch (Exception e) {
if (Messaging.isDebugging()) {
Messaging.debug("Profile lookup for player '" +
profile.getName() + "' failed: " + getExceptionMsg(e));
}
if (isTooManyRequests(e)) {
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
} else {
request.setResult(null, ProfileFetchResult.FAILED);
}
}
}
});
}
@Nullable
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
name = name.toLowerCase();
for (ProfileRequest request : requests) {
if (request.getPlayerName().equals(name))
return request;
}
return null;
}
private static boolean isProfileNotFound(Exception e) {
String message = e.getMessage();
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
return (message != null && message.contains("did not find"))
|| (cause != null && cause.contains("did not find"));
}
private static String getExceptionMsg(Exception e) {
String message = e.getMessage();
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
return cause != null ? cause : message;
}
private static boolean isTooManyRequests(Exception e) {
String message = e.getMessage();
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
return (message != null && message.contains("too many requests"))
|| (cause != null && cause.contains("too many requests"));
}
private static ProfileFetchThread PROFILE_THREAD;
}

View File

@ -0,0 +1,117 @@
package net.citizensnpcs.npc.profile;
import java.util.ArrayDeque;
import java.util.Deque;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.api.CitizensAPI;
import org.bukkit.Bukkit;
/**
* Stores basic information about a single profile used to request
* profiles from the Mojang servers.
*
* <p>Also stores the result of the request.</p>
*/
public class ProfileRequest {
private final String playerName;
private Deque<ProfileFetchHandler> handlers;
private GameProfile profile;
private volatile ProfileFetchResult result = ProfileFetchResult.PENDING;
/**
* Constructor.
*
* @param playerName The name of the player whose profile is being requested.
* @param handler Optional handler to handle the result for the profile.
* Handler always invoked from the main thread.
*/
ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(playerName);
this.playerName = playerName;
if (handler != null)
addHandler(handler);
}
/**
* Get the name of the player the requested profile belongs to.
*/
public String getPlayerName() {
return playerName;
}
/**
* Get the game profile that was requested.
*
* @return The game profile or null if the profile has not been retrieved
* yet or there was an error while retrieving the profile.
*/
@Nullable
public GameProfile getProfile() {
return profile;
}
/**
* Get the result of the profile fetch.
*/
public ProfileFetchResult getResult() {
return result;
}
/**
* Add one time result handler.
*
* <p>Handler is always invoked from the main thread.</p>
*
* @param handler The result handler.
*/
public void addHandler(ProfileFetchHandler handler) {
Preconditions.checkNotNull(handler);
if (result != ProfileFetchResult.PENDING) {
handler.onResult(this);
return;
}
if (handlers == null)
handlers = new ArrayDeque<ProfileFetchHandler>();
handlers.addLast(handler);
}
/**
* Invoked to set the profile result.
*
* <p>Can be invoked from any thread, always executes on the main thread.</p>
*
* @param profile The profile. Null if there was an error.
* @param result The result of the request.
*/
void setResult(final @Nullable GameProfile profile, final ProfileFetchResult result) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
ProfileRequest.this.profile = profile;
ProfileRequest.this.result = result;
if (handlers == null)
return;
while (!handlers.isEmpty()) {
handlers.removeFirst().onResult(ProfileRequest.this);
}
handlers = null;
}
});
}
}

View File

@ -0,0 +1,157 @@
package net.citizensnpcs.npc.skin;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import com.google.common.base.Preconditions;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.util.NMS;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* Sends remove packets in batch per player.
*
* <p>Collects entities to remove and sends them all to the
* player in a single packet.</p>
*/
public class PlayerListRemover {
private final Map<UUID, PlayerEntry> pending =
new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
PlayerListRemover() {
Bukkit.getScheduler().runTaskTimer(CitizensAPI.getPlugin(), new Sender(), 2, 2);
}
/**
* Send a remove packet to the specified player for the specified
* skinnable entity.
*
* @param player The player to send the packet to.
* @param entity The entity to remove.
*/
public void sendPacket(Player player, SkinnableEntity entity) {
Preconditions.checkNotNull(player);
Preconditions.checkNotNull(entity);
PlayerEntry entry = getEntry(player);
entry.toRemove.add(entity);
}
/**
* Cancel packets pending to be sent to the specified player.
*
* @param player The player.
*/
public void cancelPackets(Player player) {
Preconditions.checkNotNull(player);
PlayerEntry entry = pending.remove(player.getUniqueId());
if (entry == null)
return;
for (SkinnableEntity entity : entry.toRemove) {
entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
}
}
/**
* Cancel packets pending to be sent to the specified player
* for the specified skinnable entity.
*
* @param player The player.
* @param skinnable The skinnable entity.
*/
public void cancelPackets(Player player, SkinnableEntity skinnable) {
Preconditions.checkNotNull(player);
Preconditions.checkNotNull(skinnable);
PlayerEntry entry = pending.get(player.getUniqueId());
if (entry == null)
return;
if (entry.toRemove.remove(skinnable)) {
skinnable.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
}
if (entry.toRemove.isEmpty())
pending.remove(player.getUniqueId());
}
private PlayerEntry getEntry(Player player) {
PlayerEntry entry = pending.get(player.getUniqueId());
if (entry == null) {
entry = new PlayerEntry(player);
pending.put(player.getUniqueId(), entry);
}
return entry;
}
private class PlayerEntry {
Player player;
Set<SkinnableEntity> toRemove = new HashSet<SkinnableEntity>(25);
PlayerEntry(Player player) {
this.player = player;
}
}
private class Sender implements Runnable {
@Override
public void run() {
int maxPacketEntries = Settings.Setting.MAX_PACKET_ENTRIES.asInt();
Iterator<Map.Entry<UUID, PlayerEntry>> entryIterator = pending.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<UUID, PlayerEntry> mapEntry = entryIterator.next();
PlayerEntry entry = mapEntry.getValue();
int listSize = Math.min(maxPacketEntries, entry.toRemove.size());
boolean sendAll = listSize == entry.toRemove.size();
List<SkinnableEntity> skinnableList = new ArrayList<SkinnableEntity>(listSize);
int i =0;
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
while (skinIterator.hasNext()) {
if (i >= maxPacketEntries)
break;
SkinnableEntity skinnable = skinIterator.next();
skinnableList.add(skinnable);
skinIterator.remove();
i++;
}
if (entry.player.isOnline())
NMS.sendPlayerListRemove(entry.player, skinnableList);
// notify skin trackers that a remove packet has been sent to a player
for (SkinnableEntity entity : skinnableList) {
entity.getSkinTracker().notifyRemovePacketSent(entry.player.getUniqueId());
}
if (sendAll)
entryIterator.remove();
}
}
}
}

View File

@ -0,0 +1,241 @@
package net.citizensnpcs.npc.skin;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.event.DespawnReason;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.npc.profile.ProfileFetchResult;
import net.citizensnpcs.npc.profile.ProfileFetchHandler;
import net.citizensnpcs.npc.profile.ProfileFetcher;
import net.citizensnpcs.npc.profile.ProfileRequest;
/**
* Stores data for a single skin.
*/
public class Skin {
private final String skinName;
private volatile Property skinData;
private volatile UUID skinId;
private volatile boolean isValid = true;
private final Map<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(15);
/**
* Get a skin for a skinnable entity.
*
* <p>If a Skin instance does not exist, a new one is created and the
* skin data is automatically fetched.</p>
*
* @param entity The skinnable entity.
*/
public static Skin get(SkinnableEntity entity) {
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;
}
/**
* Constructor.
*
* @param skinName The name of the player the skin belongs to.
*/
Skin(String skinName) {
this.skinName = skinName.toLowerCase();
synchronized (CACHE) {
if (CACHE.containsKey(this.skinName))
throw new IllegalArgumentException("There is already a skin named " + skinName);
CACHE.put(this.skinName, this);
}
ProfileFetcher.fetch(this.skinName, new ProfileFetchHandler() {
@Override
public void onResult(ProfileRequest request) {
if (request.getResult() == ProfileFetchResult.NOT_FOUND) {
isValid = false;
return;
}
if (request.getResult() == ProfileFetchResult.SUCCESS) {
GameProfile profile = request.getProfile();
setData(profile);
}
}
});
}
/**
* Get the name of the skin.
*/
public String getSkinName() {
return skinName;
}
/**
* Get the ID of the player the skin belongs to.
*
* @return The skin ID or null if it has not been retrieved yet or
* the skin is invalid.
*/
@Nullable
public UUID getSkinId() {
return skinId;
}
/**
* Determine if the skin is valid.
*/
public boolean isValid() {
return isValid;
}
/**
* Determine if the skin data has been retrieved.
*/
public boolean hasSkinData() {
return skinData != null;
}
/**
* Apply the skin data to the specified skinnable entity.
*
* <p>If invoked before the skin data is ready, the skin is retrieved
* and the skin is automatically applied to the entity at a later time.</p>
*
* @param entity The skinnable entity.
*
* @return True if the skin data was available and applied, false if
* the data is being retrieved.
*/
public boolean apply(SkinnableEntity entity) {
Preconditions.checkNotNull(entity);
NPC npc = entity.getNPC();
if (!hasSkinData()) {
// Use npc cached skin if available.
// If npc requires latest skin, cache is used for faster
// availability until the latest skin can be loaded.
String cachedName = npc.data().get(CACHED_SKIN_UUID_NAME_METADATA);
if (this.skinName.equals(cachedName)) {
skinData = new Property(this.skinName,
npc.data().<String>get(PLAYER_SKIN_TEXTURE_PROPERTIES),
npc.data().<String>get(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN));
skinId = UUID.fromString(npc.data().<String>get(CACHED_SKIN_UUID_METADATA));
setNPCSkinData(entity, skinName, skinId, skinData);
// check if NPC prefers to use cached skin over the latest skin.
if (!entity.getNPC().data().get("update-skin",
Settings.Setting.NPC_SKIN_UPDATE.asBoolean())) {
// cache preferred
return true;
}
if (!Settings.Setting.NPC_SKIN_UPDATE.asBoolean()) {
// cache preferred
return true;
}
}
pending.put(entity, null);
return false;
}
setNPCSkinData(entity, skinName, skinId, skinData);
return true;
}
/**
* Apply the skin data to the specified skinnable entity
* and respawn the NPC.
*
* @param entity The skinnable entity.
*/
public void applyAndRespawn(SkinnableEntity entity) {
Preconditions.checkNotNull(entity);
if (!apply(entity))
return;
NPC npc = entity.getNPC();
if (npc.isSpawned()) {
npc.despawn(DespawnReason.PENDING_RESPAWN);
npc.spawn(npc.getStoredLocation());
}
}
private void setData(@Nullable GameProfile profile) {
if (profile == null) {
isValid = false;
return;
}
if (!profile.getName().toLowerCase().equals(skinName)) {
throw new IllegalArgumentException(
"GameProfile name (" + profile.getName() + ") and "
+ "skin name (" + skinName + ") do not match.");
}
skinId = profile.getId();
skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
for (SkinnableEntity entity : pending.keySet()) {
applyAndRespawn(entity);
}
}
private static void setNPCSkinData(SkinnableEntity entity,
String skinName, UUID skinId, Property skinProperty) {
NPC npc = entity.getNPC();
// cache skins for faster initial skin availability
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName);
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString());
npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES, skinProperty.getValue());
npc.data().setPersistent(PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN, skinProperty.getSignature());
GameProfile profile = entity.getProfile();
profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties.
profile.getProperties().put("textures", skinProperty);
}
public static final String PLAYER_SKIN_TEXTURE_PROPERTIES = "player-skin-textures";
public static final String PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN = "player-skin-signature";
public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
public static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
private static final Map<String, Skin> CACHE = new HashMap<String, Skin>(20);
}

View File

@ -0,0 +1,250 @@
package net.citizensnpcs.npc.skin;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import com.google.common.base.Preconditions;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.util.NMS;
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.BukkitTask;
/**
* Handles and synchronizes add and remove packets for Player type NPC's
* in order to properly apply the NPC skin.
*
* <p>Used as one instance per NPC entity.</p>
*/
public class SkinPacketTracker {
private final SkinnableEntity entity;
private final Map<UUID, PlayerEntry> inProgress =
new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
private Skin skin;
private boolean isRemoved;
/**
* Constructor.
*
* @param entity The skinnable entity the instance belongs to.
*/
public SkinPacketTracker(SkinnableEntity entity) {
Preconditions.checkNotNull(entity);
this.entity = entity;
this.skin = Skin.get(entity);
if (LISTENER == null) {
LISTENER = new PlayerListener();
Bukkit.getPluginManager().registerEvents(LISTENER, CitizensAPI.getPlugin());
}
}
/**
* Get the NPC skin.
*/
public Skin getSkin() {
return skin;
}
/**
* Send skin related packets to a player.
*
* @param player The player.
*/
public void updateViewer(final Player player) {
Preconditions.checkNotNull(player);
if (isRemoved || player.hasMetadata("NPC"))
return;
PlayerEntry entry = inProgress.get(player.getUniqueId());
if (entry != null) {
entry.cancel();
}
else {
entry = new PlayerEntry(player);
}
PLAYER_LIST_REMOVER.cancelPackets(player, entity);
inProgress.put(player.getUniqueId(), entry);
skin.apply(entity);
NMS.sendPlayerListAdd(player, entity.getBukkitEntity());
scheduleRemovePacket(entry, 2);
}
/**
* Send skin related packets to all nearby players within the specified block radius.
*
* @param radius The radius.
*/
public void updateNearbyViewers(double radius) {
radius *= radius;
org.bukkit.World world = entity.getBukkitEntity().getWorld();
Player from = entity.getBukkitEntity();
Location location = from.getLocation();
for (Player player : Bukkit.getServer().getOnlinePlayers()) {
if (player == null || player.hasMetadata("NPC"))
continue;
if (world != player.getWorld() || !player.canSee(from))
continue;
if (location.distanceSquared(player.getLocation(CACHE_LOCATION)) > radius)
continue;
updateViewer(player);
}
}
/**
* Invoke when the NPC entity is removed.
*
* <p>Sends remove packets to all players.</p>
*/
public void onRemoveNPC() {
isRemoved = true;
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
// send packet now and later to ensure removal from player list
NMS.sendPlayerListRemove(player, entity.getBukkitEntity());
PLAYER_LIST_REMOVER.sendPacket(player, entity);
}
}
/**
* Notify that the NPC skin has been changed.
*/
public void notifySkinChange() {
this.skin = Skin.get(entity);
skin.applyAndRespawn(entity);
}
/**
* Notify the tracker that a remove packet has been sent to the
* specified player.
*
* @param playerId The ID of the player.
*/
void notifyRemovePacketSent(UUID playerId) {
PlayerEntry entry = inProgress.get(playerId);
if (entry == null)
return;
if (entry.removeCount == 0)
return;
entry.removeCount -= 1;
if (entry.removeCount == 0) {
inProgress.remove(playerId);
}
else {
scheduleRemovePacket(entry);
}
}
/**
* Notify the tracker that a remove packet has been sent to the
* specified player.
*
* @param playerId The ID of the player.
*/
void notifyRemovePacketCancelled(UUID playerId) {
inProgress.remove(playerId);
}
private void scheduleRemovePacket(PlayerEntry entry, int count) {
if (!shouldRemoveFromPlayerList())
return;
entry.removeCount = count;
scheduleRemovePacket(entry);
}
private void scheduleRemovePacket(final PlayerEntry entry) {
if (isRemoved)
return;
entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(),
new Runnable() {
@Override
public void run() {
if (shouldRemoveFromPlayerList()) {
PLAYER_LIST_REMOVER.sendPacket(entry.player, entity);
}
}
}, PACKET_DELAY_REMOVE);
}
private boolean shouldRemoveFromPlayerList() {
boolean isTablistDisabled = Settings.Setting.DISABLE_TABLIST.asBoolean();
boolean isNpcRemoved = entity.getNPC().data().get("removefromplayerlist",
Settings.Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean());
return isNpcRemoved && isTablistDisabled;
}
private class PlayerEntry {
Player player;
int removeCount;
BukkitTask removeTask;
PlayerEntry (Player player) {
this.player = player;
}
// cancel previous packet tasks so they do not interfere with
// new tasks
void cancel() {
if (removeTask != null)
removeTask.cancel();
removeCount = 0;
}
}
private static class PlayerListener implements Listener {
@EventHandler
private void onPlayerQuit(PlayerQuitEvent event) {
// this also causes any entries in the "inProgress" field to
// be removed.
PLAYER_LIST_REMOVER.cancelPackets(event.getPlayer());
}
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static final int PACKET_DELAY_REMOVE = 1;
private static final PlayerListRemover PLAYER_LIST_REMOVER = new PlayerListRemover();
private static PlayerListener LISTENER;
}

View File

@ -0,0 +1,40 @@
package net.citizensnpcs.npc.skin;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.npc.ai.NPCHolder;
import org.bukkit.entity.Player;
/**
* Interface for player entities that are skinnable.
*/
public interface SkinnableEntity extends NPCHolder {
/**
* Get the entities skin packet tracker.
*/
SkinPacketTracker getSkinTracker();
/**
* Get the bukkit entity.
*/
Player getBukkitEntity();
/**
* Get entity game profile.
*/
GameProfile getProfile();
/**
* Get the name of the player whose skin the NPC uses.
*/
String getSkinName();
/**
* Set the name of the player whose skin the NPC
* uses.
*
* <p>Setting the skin name automatically updates and
* respawn the NPC.</p>
*/
void setSkinName(String name);
}

View File

@ -2,7 +2,9 @@ package net.citizensnpcs.util;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -10,33 +12,25 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import javax.annotation.Nullable;
import org.apache.commons.lang.Validate; import com.google.common.base.Preconditions;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
import org.bukkit.craftbukkit.v1_8_R3.CraftSound;
import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.plugin.PluginLoadOrder;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.mojang.authlib.GameProfileRepository;
import com.mojang.authlib.HttpAuthenticationService;
import com.mojang.authlib.minecraft.MinecraftSessionService;
import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse;
import com.mojang.util.UUIDTypeAdapter;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.command.exception.CommandException; import net.citizensnpcs.api.command.exception.CommandException;
import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.npc.ai.NPCHolder; import net.citizensnpcs.npc.ai.NPCHolder;
import net.citizensnpcs.npc.entity.EntityHumanNPC; import net.citizensnpcs.npc.entity.EntityHumanNPC;
import net.citizensnpcs.npc.network.EmptyChannel; import net.citizensnpcs.npc.network.EmptyChannel;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.util.nms.PlayerlistTrackerEntry; import net.citizensnpcs.util.nms.PlayerlistTrackerEntry;
import net.minecraft.server.v1_8_R3.AttributeInstance; import net.minecraft.server.v1_8_R3.AttributeInstance;
import net.minecraft.server.v1_8_R3.Block; import net.minecraft.server.v1_8_R3.Block;
@ -60,28 +54,136 @@ import net.minecraft.server.v1_8_R3.NavigationAbstract;
import net.minecraft.server.v1_8_R3.NetworkManager; import net.minecraft.server.v1_8_R3.NetworkManager;
import net.minecraft.server.v1_8_R3.Packet; import net.minecraft.server.v1_8_R3.Packet;
import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo;
import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo.EnumPlayerInfoAction;
import net.minecraft.server.v1_8_R3.PathfinderGoalSelector; import net.minecraft.server.v1_8_R3.PathfinderGoalSelector;
import net.minecraft.server.v1_8_R3.World; import net.minecraft.server.v1_8_R3.World;
import net.minecraft.server.v1_8_R3.WorldServer; import net.minecraft.server.v1_8_R3.WorldServer;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
import org.bukkit.craftbukkit.v1_8_R3.CraftSound;
import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Horse;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.plugin.PluginLoadOrder;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class NMS { public class NMS {
private NMS() { private NMS() {
// util class // util class
} }
public static void addOrRemoveFromPlayerList(org.bukkit.entity.Entity entity, boolean remove) { public static GameProfileRepository getGameProfileRepository() {
if (entity == null) return ((CraftServer) Bukkit.getServer()).getServer()
return; .getGameProfileRepository();
EntityHuman handle = (EntityHuman) getHandle(entity); }
if (handle.world == null)
return; public static boolean addToWorld(org.bukkit.World world,
if (remove) { org.bukkit.entity.Entity entity,
handle.world.players.remove(handle); CreatureSpawnEvent.SpawnReason reason) {
} else if (!handle.world.players.contains(handle)) { Preconditions.checkNotNull(world);
handle.world.players.add(handle); Preconditions.checkNotNull(entity);
Preconditions.checkNotNull(reason);
Entity nmsEntity = ((CraftEntity)entity).getHandle();
return ((CraftWorld)world).getHandle().addEntity(nmsEntity, reason);
}
public static void removeFromWorld(org.bukkit.entity.Entity entity) {
Preconditions.checkNotNull(entity);
Entity nmsEntity = ((CraftEntity)entity).getHandle();
nmsEntity.world.removeEntity(nmsEntity);
}
@Nullable
public static SkinnableEntity getSkinnable(org.bukkit.entity.Entity entity) {
Preconditions.checkNotNull(entity);
Entity nmsEntity = ((CraftEntity) entity).getHandle();
if (nmsEntity instanceof SkinnableEntity) {
return (SkinnableEntity)nmsEntity;
} }
return null;
}
public static void sendPlayerListAdd(Player recipient, Player listPlayer) {
Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(listPlayer);
EntityPlayer entity = ((CraftPlayer)listPlayer).getHandle();
sendPacket(recipient, new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, entity));
}
public static void sendPlayerListRemove(Player recipient, Player listPlayer) {
Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(listPlayer);
EntityPlayer entity = ((CraftPlayer)listPlayer).getHandle();
sendPacket(recipient, new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entity));
}
public static void sendPlayerListRemove(Player recipient,
Collection<? extends SkinnableEntity> skinnableNPCs) {
Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(skinnableNPCs);
EntityPlayer[] entities = new EntityPlayer[skinnableNPCs.size()];
int i=0;
for (SkinnableEntity skinnable : skinnableNPCs) {
entities[i] = (EntityPlayer)skinnable;
i++;
}
sendPacket(recipient, new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entities));
}
/*
* Yggdrasil's default implementation of this method silently fails instead of throwing
* an Exception like it should.
*/
public static GameProfile fillProfileProperties(GameProfile profile,
boolean requireSecure) throws Exception {
if (Bukkit.isPrimaryThread())
throw new IllegalStateException("NMS.fillProfileProperties cannot be invoked from the main thread.");
MinecraftSessionService sessionService = ((CraftServer) Bukkit.getServer()).getServer().aD();
YggdrasilAuthenticationService auth = ((YggdrasilMinecraftSessionService) sessionService)
.getAuthenticationService();
URL url = HttpAuthenticationService.constantURL(
"https://sessionserver.mojang.com/session/minecraft/profile/" +
UUIDTypeAdapter.fromUUID(profile.getId()));
url = HttpAuthenticationService.concatenateURL(url, "unsigned=" + !requireSecure);
MinecraftProfilePropertiesResponse response = (MinecraftProfilePropertiesResponse)
MAKE_REQUEST.invoke(auth, url, null, MinecraftProfilePropertiesResponse.class);
if (response == null)
return profile;
GameProfile result = new GameProfile(response.getId(), response.getName());
result.getProperties().putAll(response.getProperties());
profile.getProperties().putAll(response.getProperties());
return result;
} }
public static void attack(EntityLiving handle, Entity target) { public static void attack(EntityLiving handle, Entity target) {
@ -506,15 +608,6 @@ public class NMS {
NMS.sendPacketsNearby(from, location, Arrays.asList(packets), 64); NMS.sendPacketsNearby(from, location, Arrays.asList(packets), 64);
} }
public static void sendPlayerlistPacket(boolean showInPlayerlist, Player npc) {
if (!showInPlayerlist && !Setting.DISABLE_TABLIST.asBoolean())
return;
PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(
showInPlayerlist ? EnumPlayerInfoAction.ADD_PLAYER : EnumPlayerInfoAction.REMOVE_PLAYER,
((CraftPlayer) npc).getHandle());
sendToOnline(packet);
}
public static void sendToOnline(Packet... packets) { public static void sendToOnline(Packet... packets) {
Validate.notNull(packets, "packets cannot be null"); Validate.notNull(packets, "packets cannot be null");
for (Player player : Bukkit.getOnlinePlayers()) { for (Player player : Bukkit.getOnlinePlayers()) {
@ -703,6 +796,7 @@ public class NMS {
private static Field SKULL_PROFILE_FIELD; private static Field SKULL_PROFILE_FIELD;
private static Field TRACKED_ENTITY_SET = NMS.getField(EntityTracker.class, "c"); private static Field TRACKED_ENTITY_SET = NMS.getField(EntityTracker.class, "c");
private static Method MAKE_REQUEST;
static { static {
try { try {
@ -713,5 +807,13 @@ public class NMS {
} catch (Exception e) { } catch (Exception e) {
Messaging.logTr(Messages.ERROR_GETTING_ID_MAPPING, e.getMessage()); Messaging.logTr(Messages.ERROR_GETTING_ID_MAPPING, e.getMessage());
} }
try {
MAKE_REQUEST = YggdrasilAuthenticationService.class.getDeclaredMethod("makeRequest", URL.class,
Object.class, Class.class);
MAKE_REQUEST.setAccessible(true);
} catch (Exception ex) {
ex.printStackTrace();
}
} }
} }

View File

@ -1,17 +1,14 @@
package net.citizensnpcs.util.nms; package net.citizensnpcs.util.nms;
import java.lang.reflect.Field; import net.citizensnpcs.npc.entity.EntityHumanNPC;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.NMS;
import net.minecraft.server.v1_8_R3.Entity; import net.minecraft.server.v1_8_R3.Entity;
import net.minecraft.server.v1_8_R3.EntityPlayer; import net.minecraft.server.v1_8_R3.EntityPlayer;
import net.minecraft.server.v1_8_R3.EntityTrackerEntry; import net.minecraft.server.v1_8_R3.EntityTrackerEntry;
import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; import org.bukkit.entity.Player;
import java.lang.reflect.Field;
public class PlayerlistTrackerEntry extends EntityTrackerEntry { public class PlayerlistTrackerEntry extends EntityTrackerEntry {
public PlayerlistTrackerEntry(Entity entity, int i, int j, boolean flag) { public PlayerlistTrackerEntry(Entity entity, int i, int j, boolean flag) {
@ -24,28 +21,26 @@ public class PlayerlistTrackerEntry extends EntityTrackerEntry {
@Override @Override
public void updatePlayer(final EntityPlayer entityplayer) { public void updatePlayer(final EntityPlayer entityplayer) {
// prevent updates to NPC "viewers"
if (entityplayer instanceof EntityHumanNPC)
return;
if (entityplayer != this.tracker && c(entityplayer)) { if (entityplayer != this.tracker && c(entityplayer)) {
if (!this.trackedPlayers.contains(entityplayer) if (!this.trackedPlayers.contains(entityplayer)
&& ((entityplayer.u().getPlayerChunkMap().a(entityplayer, this.tracker.ae, this.tracker.ag)) && ((entityplayer.u().getPlayerChunkMap().a(entityplayer, this.tracker.ae, this.tracker.ag))
|| (this.tracker.attachedToPlayer))) { || (this.tracker.attachedToPlayer))) {
if ((this.tracker instanceof EntityPlayer)) {
Player player = ((EntityPlayer) this.tracker).getBukkitEntity();
if (!entityplayer.getBukkitEntity().canSee(player)) {
return;
}
entityplayer.playerConnection.sendPacket(new PacketPlayOutPlayerInfo(
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, (EntityPlayer) this.tracker));
if (Setting.DISABLE_TABLIST.asBoolean()) { if ((this.tracker instanceof SkinnableEntity)) {
new BukkitRunnable() {
@Override SkinnableEntity skinnable = (SkinnableEntity)this.tracker;
public void run() {
entityplayer.playerConnection.sendPacket(new PacketPlayOutPlayerInfo( Player player = skinnable.getBukkitEntity();
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, if (!entityplayer.getBukkitEntity().canSee(player))
(EntityPlayer) tracker)); return;
}
}.runTaskLater(CitizensAPI.getPlugin(), 2); skinnable.getSkinTracker().updateViewer(entityplayer.getBukkitEntity());
}
} }
} }
} }