mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2025-01-27 10:31:21 +01:00
improve player NPC skins
add skin packages with skin classes add profile package with profile fetcher classes update commands update EventListen update NMS fix radius squared fix exception message fix npc sometimes not removed from playerlist fix cannot add npc to playerlist code/comment cleanup and refactoring remove unused skin settings removed NPC_SKIN_RETRY_DELAY, MAX_NPC_SKIN_RETRIES current code uses cached textures if a skin profile request fails. add setting for updating skin added NPC_SKIN_UPDATE: true to always get the latest skin, false to use cached skin if available, default is false. minor code fixes, refactoring, add settings removed assert removed thread checks added setting: NPC_SKIN_VIEW_DISTANCE added setting: NPC_SKIN_UPDATE_DISTANCE added setting: MAX_PACKET_ENTRIES invoke EventListen#SkinUpdateTracker#reset from within #shouldUpdate instead of requiring it to be invoked manually. fix cached locations not used in EventListen#getNearbySkinnableNPCs clamp yaw in EventListen.SkinUpdateTracker use static constants rename EntitySkinnable to SkinnableEntity add SkinnableEntity interface to PlayerNPC (CraftPlayer) remove unused code from PlayerListRemover replace Subscriber with direct notification to entity via method Undo EntityController interface changes moved skin code from HumanController to CitizensNPC fix npcs sometimes do not show ... due to packet tracker not being notified that remove packets have been cancelled fix imports rearranged by incorrect IDE settings
This commit is contained in:
parent
fbfd9db7ce
commit
f8fdd55c65
@ -2,46 +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.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;
|
||||
@ -74,17 +40,53 @@ 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.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;
|
||||
@ -338,7 +340,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)
|
||||
@ -360,7 +362,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)
|
||||
@ -372,16 +374,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(ignoreCancelled = true)
|
||||
@ -423,34 +426,68 @@ 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 (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.getSkinnableNPC(npcEntity);
|
||||
|
||||
results.add(skinnable);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private void respawnAllFromCoord(ChunkCoord coord) {
|
||||
@ -472,30 +509,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) {
|
||||
@ -559,4 +572,62 @@ 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();
|
||||
|
||||
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 < 2) {
|
||||
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;
|
||||
@ -992,11 +993,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());
|
||||
@ -1315,8 +1318,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.getSkinnableNPC(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,27 +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 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;
|
||||
@ -40,13 +26,25 @@ 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;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
|
||||
public class CitizensNPC extends AbstractNPC {
|
||||
private EntityController entityController;
|
||||
@ -188,7 +186,28 @@ public class CitizensNPC extends AbstractNPC {
|
||||
entityController.spawn(at, this);
|
||||
|
||||
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, CreatureSpawnEvent.SpawnReason.CUSTOM);
|
||||
|
||||
// send skin packets, if applicable, before other NMS packets are sent
|
||||
SkinnableEntity skinnable = NMS.getSkinnableNPC(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.getSkinnableNPC(getEntity());
|
||||
if (npc == null)
|
||||
return;
|
||||
|
||||
npc.getSkinTracker().updateNearbyViewers(viewDistance);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
mcEntity.setPositionRotation(at.getX(), at.getY(), at.getZ(), at.getYaw(), at.getPitch());
|
||||
|
||||
@ -245,29 +264,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,33 @@
|
||||
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
|
||||
@ -101,24 +77,29 @@ public class HumanController extends AbstractEntityController {
|
||||
uuid = new UUID(msb, uuid.getLeastSignificantBits());
|
||||
}
|
||||
|
||||
GameProfile profile = new GameProfile(uuid, coloredName);
|
||||
updateSkin(npc, nmsWorld, profile);
|
||||
final 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();
|
||||
String teamName = UUID.randomUUID().toString().substring(0,16);
|
||||
String teamName = UUID.randomUUID().toString().substring(0, 16);
|
||||
|
||||
Team team = scoreboard.getTeam(teamName);
|
||||
if (team == null) {
|
||||
@ -134,6 +115,7 @@ public class HumanController extends AbstractEntityController {
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
|
||||
handle.getBukkitEntity().setSleepingIgnored(true);
|
||||
|
||||
return handle.getBukkitEntity();
|
||||
@ -146,235 +128,14 @@ public class HumanController extends AbstractEntityController {
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
NMS.sendPlayerlistPacket(false, getBukkitEntity());
|
||||
|
||||
NMS.removeFromWorld(getBukkitEntity());
|
||||
|
||||
SkinnableEntity npc = NMS.getSkinnableNPC(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,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,14 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
/**
|
||||
* Interface for a subscriber of the results of a profile fetch.
|
||||
*/
|
||||
public interface ProfileFetchSubscriber {
|
||||
|
||||
/**
|
||||
* Invoked when a result for a profile is ready.
|
||||
*
|
||||
* @param request The profile request that was handled.
|
||||
*/
|
||||
void onResult(ProfileRequest request);
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
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>
|
||||
*/
|
||||
public class ProfileFetchThread implements Runnable {
|
||||
|
||||
private final ProfileFetcher profileFetcher = new ProfileFetcher();
|
||||
private final Deque<ProfileRequest> queue = new LinkedList<ProfileRequest>();
|
||||
private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(35);
|
||||
private final Object sync = new Object();
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*/
|
||||
public static ProfileFetchThread get() {
|
||||
if (PROFILE_THREAD == null) {
|
||||
PROFILE_THREAD = new ProfileFetchThread();
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD,
|
||||
11, 20);
|
||||
}
|
||||
return PROFILE_THREAD;
|
||||
}
|
||||
|
||||
ProfileFetchThread() {}
|
||||
|
||||
/**
|
||||
* Fetch a profile.
|
||||
*
|
||||
* @param name The name of the player the profile belongs to.
|
||||
* @param subscriber Optional subscriber to be notified when a result is available.
|
||||
* Subscriber always invoked from the main thread.
|
||||
*/
|
||||
public void fetch(String name, @Nullable ProfileFetchSubscriber subscriber) {
|
||||
Preconditions.checkNotNull(name);
|
||||
|
||||
ProfileRequest request = requested.get(name);
|
||||
|
||||
if (request != null) {
|
||||
|
||||
if (subscriber != null) {
|
||||
|
||||
if (request.getResult() == ProfileFetchResult.PENDING) {
|
||||
request.addSubscriber(subscriber);
|
||||
}
|
||||
else {
|
||||
subscriber.onResult(request);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
request = new ProfileRequest(name, subscriber);
|
||||
|
||||
synchronized (sync) {
|
||||
queue.add(request);
|
||||
}
|
||||
|
||||
requested.put(name, request);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
List<ProfileRequest> requests;
|
||||
|
||||
synchronized (sync) {
|
||||
|
||||
if (queue.isEmpty())
|
||||
return;
|
||||
|
||||
requests = new ArrayList<ProfileRequest>(queue);
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
profileFetcher.fetch(requests);
|
||||
}
|
||||
|
||||
private static ProfileFetchThread PROFILE_THREAD;
|
||||
}
|
128
src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java
Normal file
128
src/main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java
Normal file
@ -0,0 +1,128 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
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.util.Messaging;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Fetches game profiles that include skin data from Mojang servers.
|
||||
*
|
||||
* @see ProfileFetchThread
|
||||
*/
|
||||
class ProfileFetcher {
|
||||
|
||||
/**
|
||||
* Fetch one or more profiles.
|
||||
*
|
||||
* @param requests The profile requests.
|
||||
*/
|
||||
public void fetch(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 skin '" +
|
||||
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 skin '" +
|
||||
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"));
|
||||
}
|
||||
}
|
110
src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java
Normal file
110
src/main/java/net/citizensnpcs/npc/profile/ProfileRequest.java
Normal file
@ -0,0 +1,110 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
/**
|
||||
* 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<ProfileFetchSubscriber> subscribers;
|
||||
private GameProfile profile;
|
||||
private ProfileFetchResult result = ProfileFetchResult.PENDING;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param playerName The name of the player whose profile is being requested.
|
||||
* @param subscriber Optional subscriber to be notified when a result is available
|
||||
* for the profile. Subscriber always invoked from the main thread.
|
||||
*/
|
||||
ProfileRequest(String playerName, @Nullable ProfileFetchSubscriber subscriber) {
|
||||
Preconditions.checkNotNull(playerName);
|
||||
|
||||
this.playerName = playerName;
|
||||
|
||||
if (subscriber != null)
|
||||
addSubscriber(subscriber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a result subscriber to be notified when a result is available.
|
||||
*
|
||||
* <p>Subscriber is always invoked from the main thread.</p>
|
||||
*
|
||||
* @param subscriber The subscriber.
|
||||
*/
|
||||
public void addSubscriber(ProfileFetchSubscriber subscriber) {
|
||||
Preconditions.checkNotNull(subscriber);
|
||||
|
||||
if (subscribers == null)
|
||||
subscribers = new ArrayDeque<ProfileFetchSubscriber>();
|
||||
|
||||
subscribers.addLast(subscriber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (subscribers == null)
|
||||
return;
|
||||
|
||||
while (!subscribers.isEmpty()) {
|
||||
subscribers.removeFirst().onResult(ProfileRequest.this);
|
||||
}
|
||||
|
||||
subscribers = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
156
src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java
Normal file
156
src/main/java/net/citizensnpcs/npc/skin/PlayerListRemover.java
Normal file
@ -0,0 +1,156 @@
|
||||
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
|
||||
* human NPC 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());
|
||||
|
||||
for (SkinnableEntity entity : entry.toRemove) {
|
||||
entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel packets pending to be sent to the specified player
|
||||
* for the specified skinnable NPC.
|
||||
*
|
||||
* @param player The player.
|
||||
* @param skinnable The skinnable NPC.
|
||||
*/
|
||||
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()) {
|
||||
|
||||
SkinnableEntity skinnable = skinIterator.next();
|
||||
skinnableList.add(skinnable);
|
||||
|
||||
skinIterator.remove();
|
||||
|
||||
if (i > maxPacketEntries)
|
||||
break;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
274
src/main/java/net/citizensnpcs/npc/skin/Skin.java
Normal file
274
src/main/java/net/citizensnpcs/npc/skin/Skin.java
Normal file
@ -0,0 +1,274 @@
|
||||
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.ProfileFetchSubscriber;
|
||||
import net.citizensnpcs.npc.profile.ProfileFetchThread;
|
||||
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>(30);
|
||||
|
||||
/**
|
||||
* Get a skin for a human NPC 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 human NPC 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(skinName))
|
||||
throw new IllegalArgumentException("There is already a skin named " + skinName);
|
||||
|
||||
CACHE.put(skinName, this);
|
||||
}
|
||||
|
||||
ProfileFetchThread.get().fetch(skinName, new ProfileFetchSubscriber() {
|
||||
|
||||
@Override
|
||||
public void onResult(ProfileRequest request) {
|
||||
|
||||
if (request.getResult() == ProfileFetchResult.NOT_FOUND) {
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.getResult() == ProfileFetchResult.SUCCESS) {
|
||||
|
||||
GameProfile profile = request.getProfile();
|
||||
|
||||
skinId = profile.getId();
|
||||
skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set skin data.
|
||||
*
|
||||
* @param profile The profile that contains the skin data. If set to null,
|
||||
* it's assumed that the skin is not valid.
|
||||
*
|
||||
* @throws IllegalStateException if not invoked from the main thread.
|
||||
* @throws IllegalArgumentException if the profile name does not match the skin data.
|
||||
*/
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the skin data to the specified human NPC 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 human NPC entity.
|
||||
*
|
||||
* @return True if the skin data was available and applied, false if
|
||||
* the data is being retrieved.
|
||||
*
|
||||
* @throws IllegalStateException if not invoked from the main thread.
|
||||
*/
|
||||
public boolean apply(SkinnableEntity entity) {
|
||||
Preconditions.checkNotNull(entity);
|
||||
|
||||
NPC npc = entity.getNPC();
|
||||
|
||||
if (!hasSkinData()) {
|
||||
pending.put(entity, null);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// get latest skin
|
||||
fetchSkinFor(entity);
|
||||
|
||||
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 fetchSkinFor(final SkinnableEntity entity) {
|
||||
|
||||
ProfileFetchThread.get().fetch(skinName, new ProfileFetchSubscriber() {
|
||||
|
||||
@Override
|
||||
public void onResult(ProfileRequest request) {
|
||||
|
||||
if (request.getResult() != ProfileFetchResult.SUCCESS)
|
||||
return;
|
||||
|
||||
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
|
||||
entity.getSkinTracker().updateNearbyViewers(viewDistance);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
237
src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java
Normal file
237
src/main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java
Normal file
@ -0,0 +1,237 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 human NPC 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send skin related packets to a player.
|
||||
*
|
||||
* @param player The player.
|
||||
*/
|
||||
public void updateViewer(final Player player) {
|
||||
Preconditions.checkNotNull(player);
|
||||
|
||||
if (player.hasMetadata("NPC"))
|
||||
return;
|
||||
|
||||
if (isRemoved || inProgress.containsKey(player.getUniqueId()))
|
||||
return;
|
||||
|
||||
PlayerEntry entry = new PlayerEntry(player);
|
||||
inProgress.put(player.getUniqueId(), entry);
|
||||
|
||||
PLAYER_LIST_REMOVER.cancelPackets(player, entity);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleRemovePacket(PlayerEntry entry, int count) {
|
||||
|
||||
if (!shouldRemoveFromPlayerList())
|
||||
return;
|
||||
|
||||
entry.removeCount = count;
|
||||
scheduleRemovePacket(entry);
|
||||
}
|
||||
|
||||
private void scheduleRemovePacket(final PlayerEntry entry) {
|
||||
|
||||
if (isRemoved)
|
||||
return;
|
||||
|
||||
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;
|
||||
PlayerEntry (Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -11,6 +13,15 @@ import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
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.npc.skin.SkinnableEntity;
|
||||
import org.apache.commons.lang.Validate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
@ -25,12 +36,12 @@ 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;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
@ -60,28 +71,119 @@ 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 javax.annotation.Nullable;
|
||||
|
||||
@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 getSkinnableNPC(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