mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2025-01-12 11:21:05 +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.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
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.collect.ArrayListMultimap;
|
||||
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.Messaging;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
import net.citizensnpcs.trait.Controllable;
|
||||
import net.citizensnpcs.trait.CurrentLocation;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
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 {
|
||||
private final NPCRegistry npcRegistry = CitizensAPI.getNPCRegistry();
|
||||
private final Map<String, NPCRegistry> registries;
|
||||
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) {
|
||||
this.registries = registries;
|
||||
@ -339,7 +341,7 @@ public class EventListen implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerChangeWorld(PlayerChangedWorldEvent event) {
|
||||
recalculatePlayer(event.getPlayer());
|
||||
recalculatePlayer(event.getPlayer(), 20, true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
@ -361,7 +363,7 @@ public class EventListen implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
recalculatePlayer(event.getPlayer());
|
||||
recalculatePlayer(event.getPlayer(), 20, true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
@ -373,16 +375,17 @@ public class EventListen implements Listener {
|
||||
event.getPlayer().leaveVehicle();
|
||||
}
|
||||
}
|
||||
skinUpdateTrackers.remove(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||
recalculatePlayer(event.getPlayer());
|
||||
recalculatePlayer(event.getPlayer(), 15, true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerTeleport(PlayerTeleportEvent event) {
|
||||
recalculatePlayer(event.getPlayer());
|
||||
recalculatePlayer(event.getPlayer(), 15, true);
|
||||
}
|
||||
|
||||
@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() {
|
||||
|
||||
@Override
|
||||
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) {
|
||||
@ -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) {
|
||||
Location spawn = npc.getTrait(CurrentLocation.class).getLocation();
|
||||
if (spawn == null) {
|
||||
@ -569,4 +585,65 @@ public class EventListen implements Listener {
|
||||
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),
|
||||
LOCALE("general.translation.locale", ""),
|
||||
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_TEXT_RANGE("npc.chat.options.max-text-range", 500),
|
||||
MESSAGE_COLOUR("general.color-scheme.message", "<a>"),
|
||||
NEW_PATHFINDER_OPENS_DOORS("npc.pathfinding.new-finder-open-doors", false),
|
||||
NPC_ATTACK_DISTANCE("npc.pathfinding.attack-range", 1.75 * 1.75),
|
||||
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),
|
||||
QUICK_SELECT("npc.selection.quick-select", false),
|
||||
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.List;
|
||||
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.DyeColor;
|
||||
@ -987,11 +988,13 @@ public class NPCCommands {
|
||||
boolean remove = !npc.data().get("removefromplayerlist", Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean());
|
||||
if (args.hasFlag('a')) {
|
||||
remove = false;
|
||||
} else if (args.hasFlag('r'))
|
||||
} else if (args.hasFlag('r')) {
|
||||
remove = true;
|
||||
}
|
||||
npc.data().setPersistent("removefromplayerlist", remove);
|
||||
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,
|
||||
npc.getName());
|
||||
@ -1310,8 +1313,11 @@ public class NPCCommands {
|
||||
}
|
||||
Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName);
|
||||
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.util.NMS;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
|
@ -1,28 +1,13 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
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.Throwables;
|
||||
|
||||
import net.citizensnpcs.NPCNeedsRespawnEvent;
|
||||
import net.citizensnpcs.Settings;
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
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.npc.ai.CitizensBlockBreaker;
|
||||
import net.citizensnpcs.npc.ai.CitizensNavigator;
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
import net.citizensnpcs.trait.CurrentLocation;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
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.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 {
|
||||
private EntityController entityController;
|
||||
@ -190,6 +188,27 @@ public class CitizensNPC extends AbstractNPC {
|
||||
net.minecraft.server.v1_8_R3.Entity mcEntity = ((CraftEntity) getEntity()).getHandle();
|
||||
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());
|
||||
|
||||
if (!couldSpawn) {
|
||||
@ -244,29 +263,9 @@ public class CitizensNPC extends AbstractNPC {
|
||||
if (getEntity() instanceof Player) {
|
||||
final CraftPlayer player = (CraftPlayer) getEntity();
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,13 @@ import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.event.NPCPushEvent;
|
||||
import net.citizensnpcs.api.npc.MetadataStore;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.trait.Inventory;
|
||||
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.EmptyNetworkManager;
|
||||
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.Util;
|
||||
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 org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
|
||||
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.metadata.MetadataValue;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
public class EntityHumanNPC extends EntityPlayer implements NPCHolder, SkinnableEntity {
|
||||
private PlayerControllerJump controllerJump;
|
||||
private PlayerControllerLook controllerLook;
|
||||
private PlayerControllerMove controllerMove;
|
||||
@ -58,16 +64,21 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
private PlayerNavigation navigation;
|
||||
private final CitizensNPC npc;
|
||||
private final Location packetLocationCache = new Location(null, 0, 0, 0);
|
||||
private final SkinPacketTracker skinTracker;
|
||||
|
||||
public EntityHumanNPC(MinecraftServer minecraftServer, WorldServer world, GameProfile gameProfile,
|
||||
PlayerInteractManager playerInteractManager, NPC npc) {
|
||||
PlayerInteractManager playerInteractManager, NPC npc) {
|
||||
super(minecraftServer, world, gameProfile, playerInteractManager);
|
||||
|
||||
this.npc = (CitizensNPC) npc;
|
||||
if (npc != null) {
|
||||
skinTracker = new SkinPacketTracker(this);
|
||||
playerInteractManager.setGameMode(EnumGamemode.SURVIVAL);
|
||||
initialise(minecraftServer);
|
||||
}
|
||||
else {
|
||||
skinTracker = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -185,6 +196,31 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
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) {
|
||||
Socket socket = new EmptySocket();
|
||||
NetworkManager conn = null;
|
||||
@ -296,6 +332,7 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
}
|
||||
|
||||
private void updatePackets(boolean navigating) {
|
||||
|
||||
if (world.getWorld().getFullTime() % Setting.PACKET_UPDATE_DELAY.asInt() == 0) {
|
||||
// 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
|
||||
@ -316,10 +353,6 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -328,7 +361,7 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
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 CitizensNPC npc;
|
||||
|
||||
@ -372,6 +405,26 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder {
|
||||
public void setMetadata(String metadataKey, MetadataValue 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;
|
||||
|
@ -1,57 +1,32 @@
|
||||
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.concurrent.BlockingDeque;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
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.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.craftbukkit.v1_8_R3.CraftServer;
|
||||
import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
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 HumanController() {
|
||||
super();
|
||||
if (SKIN_THREAD == null) {
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), SKIN_THREAD = new SkinThread(),
|
||||
10, 10);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -100,20 +75,26 @@ public class HumanController extends AbstractEntityController {
|
||||
msb |= 0x0000000000002000L;
|
||||
uuid = new UUID(msb, uuid.getLeastSignificantBits());
|
||||
}
|
||||
GameProfile profile = new GameProfile(uuid, coloredName);
|
||||
updateSkin(npc, nmsWorld, profile);
|
||||
|
||||
GameProfile profile = new GameProfile(uuid, coloredName);
|
||||
|
||||
final EntityHumanNPC handle = new EntityHumanNPC(nmsWorld.getServer().getServer(), nmsWorld, profile,
|
||||
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());
|
||||
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (getBukkitEntity() == null || !getBukkitEntity().isValid())
|
||||
return;
|
||||
boolean removeFromPlayerList = Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean();
|
||||
NMS.addOrRemoveFromPlayerList(getBukkitEntity(),
|
||||
npc.data().get("removefromplayerlist", removeFromPlayerList));
|
||||
|
||||
if (prefixCapture != null) {
|
||||
Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard();
|
||||
@ -133,6 +114,7 @@ public class HumanController extends AbstractEntityController {
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
|
||||
handle.getBukkitEntity().setSleepingIgnored(true);
|
||||
|
||||
return handle.getBukkitEntity();
|
||||
@ -145,235 +127,14 @@ public class HumanController extends AbstractEntityController {
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
NMS.sendPlayerlistPacket(false, getBukkitEntity());
|
||||
|
||||
NMS.removeFromWorld(getBukkitEntity());
|
||||
|
||||
SkinnableEntity npc = NMS.getSkinnable(getBukkitEntity());
|
||||
npc.getSkinTracker().onRemoveNPC();
|
||||
|
||||
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 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.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -10,33 +12,25 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
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.inventory.meta.SkullMeta;
|
||||
import org.bukkit.plugin.PluginLoadOrder;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
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.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.npc.ai.NPCHolder;
|
||||
import net.citizensnpcs.npc.entity.EntityHumanNPC;
|
||||
import net.citizensnpcs.npc.network.EmptyChannel;
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
import net.citizensnpcs.util.nms.PlayerlistTrackerEntry;
|
||||
import net.minecraft.server.v1_8_R3.AttributeInstance;
|
||||
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.Packet;
|
||||
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.World;
|
||||
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")
|
||||
public class NMS {
|
||||
|
||||
private NMS() {
|
||||
// util class
|
||||
}
|
||||
|
||||
public static void addOrRemoveFromPlayerList(org.bukkit.entity.Entity entity, boolean remove) {
|
||||
if (entity == null)
|
||||
return;
|
||||
EntityHuman handle = (EntityHuman) getHandle(entity);
|
||||
if (handle.world == null)
|
||||
return;
|
||||
if (remove) {
|
||||
handle.world.players.remove(handle);
|
||||
} else if (!handle.world.players.contains(handle)) {
|
||||
handle.world.players.add(handle);
|
||||
public static GameProfileRepository getGameProfileRepository() {
|
||||
return ((CraftServer) Bukkit.getServer()).getServer()
|
||||
.getGameProfileRepository();
|
||||
}
|
||||
|
||||
public static boolean addToWorld(org.bukkit.World world,
|
||||
org.bukkit.entity.Entity entity,
|
||||
CreatureSpawnEvent.SpawnReason reason) {
|
||||
Preconditions.checkNotNull(world);
|
||||
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) {
|
||||
@ -506,15 +608,6 @@ public class NMS {
|
||||
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) {
|
||||
Validate.notNull(packets, "packets cannot be null");
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
@ -703,6 +796,7 @@ public class NMS {
|
||||
private static Field SKULL_PROFILE_FIELD;
|
||||
|
||||
private static Field TRACKED_ENTITY_SET = NMS.getField(EntityTracker.class, "c");
|
||||
private static Method MAKE_REQUEST;
|
||||
|
||||
static {
|
||||
try {
|
||||
@ -713,5 +807,13 @@ public class NMS {
|
||||
} catch (Exception e) {
|
||||
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;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.npc.entity.EntityHumanNPC;
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.minecraft.server.v1_8_R3.Entity;
|
||||
import net.minecraft.server.v1_8_R3.EntityPlayer;
|
||||
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 PlayerlistTrackerEntry(Entity entity, int i, int j, boolean flag) {
|
||||
@ -24,28 +21,26 @@ public class PlayerlistTrackerEntry extends EntityTrackerEntry {
|
||||
|
||||
@Override
|
||||
public void updatePlayer(final EntityPlayer entityplayer) {
|
||||
|
||||
// prevent updates to NPC "viewers"
|
||||
if (entityplayer instanceof EntityHumanNPC)
|
||||
return;
|
||||
|
||||
if (entityplayer != this.tracker && c(entityplayer)) {
|
||||
|
||||
if (!this.trackedPlayers.contains(entityplayer)
|
||||
&& ((entityplayer.u().getPlayerChunkMap().a(entityplayer, this.tracker.ae, this.tracker.ag))
|
||||
|| (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()) {
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
entityplayer.playerConnection.sendPacket(new PacketPlayOutPlayerInfo(
|
||||
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER,
|
||||
(EntityPlayer) tracker));
|
||||
}
|
||||
}.runTaskLater(CitizensAPI.getPlugin(), 2);
|
||||
}
|
||||
if ((this.tracker instanceof SkinnableEntity)) {
|
||||
|
||||
SkinnableEntity skinnable = (SkinnableEntity)this.tracker;
|
||||
|
||||
Player player = skinnable.getBukkitEntity();
|
||||
if (!entityplayer.getBukkitEntity().canSee(player))
|
||||
return;
|
||||
|
||||
skinnable.getSkinTracker().updateViewer(entityplayer.getBukkitEntity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user