mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2024-11-04 01:39:54 +01:00
Merge pull request #490 from JCThePants/skins2
Improve player NPC skins
This commit is contained in:
commit
7ff746a5b6
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
154
src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java
Normal file
154
src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java
Normal 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;
|
||||||
|
}
|
117
src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java
Normal file
117
src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
157
src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java
Normal file
157
src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
241
src/main/java/net/citizensnpcs/npc/skin/Skin.java
Normal file
241
src/main/java/net/citizensnpcs/npc/skin/Skin.java
Normal 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);
|
||||||
|
}
|
250
src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java
Normal file
250
src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java
Normal 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;
|
||||||
|
}
|
40
src/main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java
Normal file
40
src/main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java
Normal 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);
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user