Merge branch 'master' of github.com:CitizensDev/Citizens2

This commit is contained in:
fullwall 2015-10-21 15:06:25 +08:00
commit 56fe2bb453
14 changed files with 858 additions and 230 deletions

View File

@ -1,8 +1,6 @@
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;
@ -17,6 +15,8 @@ import com.mojang.authlib.properties.Property;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.event.NavigationBeginEvent;
import net.citizensnpcs.api.ai.event.NavigationCompleteEvent;
import net.citizensnpcs.api.event.CitizensDeserialiseMetaEvent;
import net.citizensnpcs.api.event.CitizensReloadEvent;
import net.citizensnpcs.api.event.CitizensSerialiseMetaEvent;
@ -42,17 +42,17 @@ 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.npc.skin.SkinUpdateTracker;
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.Navigation;
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;
@ -81,19 +81,17 @@ 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.scheduler.BukkitTask;
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);
private final SkinUpdateTracker skinUpdateTracker;
EventListen(Map<String, NPCRegistry> registries) {
this.registries = registries;
this.skinUpdateTracker = new SkinUpdateTracker(npcRegistry, registries);
}
private void checkCreationEvent(CommandSenderCreateNPCEvent event) {
@ -326,12 +324,7 @@ public class EventListen implements Listener {
@EventHandler
public void onNPCSpawn(NPCSpawnEvent event) {
SkinnableEntity skinnable = NMS.getSkinnable(event.getNPC().getEntity());
if (skinnable == null)
return;
// reset nearby players in case they are not looking at the NPC when it spawns.
resetNearbyPlayers(skinnable);
skinUpdateTracker.onNPCSpawn(event.getNPC());
}
@EventHandler
@ -341,6 +334,17 @@ public class EventListen implements Listener {
toRespawn.remove(toCoord(event.getNPC().getStoredLocation()), event.getNPC());
}
}
skinUpdateTracker.onNPCDespawn(event.getNPC());
}
@EventHandler
public void onNavigationBegin(NavigationBeginEvent event) {
skinUpdateTracker.onNPCNavigationBegin(event.getNPC());
}
@EventHandler
public void onNavigationComplete(NavigationCompleteEvent event) {
skinUpdateTracker.onNPCNavigationComplete(event.getNPC());
}
@EventHandler(ignoreCancelled = true)
@ -354,7 +358,7 @@ public class EventListen implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangeWorld(PlayerChangedWorldEvent event) {
recalculatePlayer(event.getPlayer(), 20, true);
skinUpdateTracker.updatePlayer(event.getPlayer(), 20, true);
}
@EventHandler(ignoreCancelled = true)
@ -376,7 +380,7 @@ public class EventListen implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
recalculatePlayer(event.getPlayer(), 20, true);
skinUpdateTracker.updatePlayer(event.getPlayer(), 20, true);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
@ -388,17 +392,17 @@ public class EventListen implements Listener {
event.getPlayer().leaveVehicle();
}
}
skinUpdateTrackers.remove(event.getPlayer().getUniqueId());
skinUpdateTracker.removePlayer(event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerRespawn(PlayerRespawnEvent event) {
recalculatePlayer(event.getPlayer(), 15, true);
skinUpdateTracker.updatePlayer(event.getPlayer(), 15, true);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerTeleport(PlayerTeleportEvent event) {
recalculatePlayer(event.getPlayer(), 15, true);
skinUpdateTracker.updatePlayer(event.getPlayer(), 15, true);
}
@EventHandler
@ -453,108 +457,12 @@ public class EventListen implements Listener {
// a player moves a certain distance from their last position.
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(final PlayerMoveEvent event) {
SkinUpdateTracker updateTracker = skinUpdateTrackers.get(event.getPlayer().getUniqueId());
if (updateTracker == null)
return;
if (!updateTracker.shouldUpdate(event.getPlayer()))
return;
recalculatePlayer(event.getPlayer(), 10, false);
skinUpdateTracker.onPlayerMove(event.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onCitizensReload(CitizensReloadEvent event) {
skinUpdateTrackers.clear();
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasMetadata("NPC"))
continue;
SkinUpdateTracker tracker = skinUpdateTrackers.get(player.getUniqueId());
if (tracker == null)
continue;
tracker.hardReset(player);
}
}
public void recalculatePlayer(final Player player, long delay, boolean reset) {
if (player.hasMetadata("NPC"))
return;
SkinUpdateTracker tracker = skinUpdateTrackers.get(player.getUniqueId());
if (tracker == null) {
tracker = new SkinUpdateTracker(player);
skinUpdateTrackers.put(player.getUniqueId(), tracker);
}
else if (reset) {
tracker.hardReset(player);
}
new BukkitRunnable() {
@Override
public void run() {
List<SkinnableEntity> nearbyNPCs = getNearbySkinnableNPCs(player);
for (SkinnableEntity npc : nearbyNPCs) {
npc.getSkinTracker().updateViewer(player);
}
}
}.runTaskLater(CitizensAPI.getPlugin(), delay);
}
// hard reset skin update trackers for players near a skinnable NPC
private void resetNearbyPlayers(SkinnableEntity skinnable) {
Entity entity = skinnable.getBukkitEntity();
if (entity == null || !entity.isValid())
return;
double viewDistance = Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
viewDistance *= viewDistance;
Location location = entity.getLocation(NPC_LOCATION);
List<Player> players = entity.getWorld().getPlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
double distanceSquared = player.getLocation(CACHE_LOCATION).distanceSquared(location);
if (distanceSquared > viewDistance)
continue;
SkinUpdateTracker tracker = skinUpdateTrackers.get(player.getUniqueId());
if (tracker == null) {
tracker = new SkinUpdateTracker(player);
skinUpdateTrackers.put(player.getUniqueId(), tracker);
}
else {
tracker.hardReset(player);
}
}
}
private List<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;
skinUpdateTracker.reset();
}
private void respawnAllFromCoord(ChunkCoord coord) {
@ -639,72 +547,4 @@ public class EventListen implements Listener {
return prime * (prime * (prime + ((worldName == null) ? 0 : worldName.hashCode())) + x) + z;
}
}
private class SkinUpdateTracker {
final Location location = new Location(null, 0, 0, 0);
int rotationCount;
boolean hasMoved;
float upperBound;
float lowerBound;
SkinUpdateTracker(Player player) {
hardReset(player);
}
boolean shouldUpdate(Player player) {
Location currentLoc = player.getLocation(CACHE_LOCATION);
if (!hasMoved) {
hasMoved = true;
return true;
}
if (rotationCount < 3) {
float yaw = NMS.clampYaw(currentLoc.getYaw());
boolean hasRotated = upperBound < lowerBound
? yaw > upperBound && yaw < lowerBound
: yaw > upperBound || yaw < lowerBound;
// 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(this.location);
if (rotationCount < 3) {
float rotationDegrees = Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat();
float yaw = NMS.clampYaw(this.location.getYaw());
this.upperBound = NMS.clampYaw(yaw + rotationDegrees);
this.lowerBound = NMS.clampYaw(yaw - rotationDegrees);
}
}
void hardReset(Player player) {
this.hasMoved = false;
this.rotationCount = 0;
reset(player);
}
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
private static final int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50;
}

View File

@ -79,6 +79,8 @@ import net.citizensnpcs.trait.Powered;
import net.citizensnpcs.trait.RabbitType;
import net.citizensnpcs.trait.RabbitType.RabbitTypes;
import net.citizensnpcs.trait.SheepTrait;
import net.citizensnpcs.trait.SkinLayers;
import net.citizensnpcs.trait.SkinLayers.Layer;
import net.citizensnpcs.trait.SlimeSize;
import net.citizensnpcs.trait.VillagerProfession;
import net.citizensnpcs.trait.WolfModifiers;
@ -1322,6 +1324,42 @@ public class NPCCommands {
}
}
@Command(
aliases = { "npc" },
usage = "skinlayers (--cape [true|false]) (--hat [true|false]) (--jacket [true|false]) (--sleeves [true|false]) (--pants [true|false])",
desc = "Sets an NPC's skin layers visibility.",
modifiers = { "skinlayers" },
min = 1,
max = 5,
permission = "citizens.npc.skinlayers")
@Requirements(types = EntityType.PLAYER, selected = true, ownership = true)
public void skinLayers(final CommandContext args, final CommandSender sender, final NPC npc) throws CommandException {
SkinLayers trait = npc.getTrait(SkinLayers.class);
if (args.hasValueFlag("cape")) {
trait.setVisible(Layer.CAPE, Boolean.valueOf(args.getFlag("cape")));
}
if (args.hasValueFlag("hat")) {
trait.setVisible(Layer.HAT, Boolean.valueOf(args.getFlag("hat")));
}
if (args.hasValueFlag("jacket")) {
trait.setVisible(Layer.JACKET, Boolean.valueOf(args.getFlag("jacket")));
}
if (args.hasValueFlag("sleeves")) {
boolean hasSleeves = Boolean.valueOf(args.getFlag("sleeves"));
trait.setVisible(Layer.LEFT_SLEEVE, hasSleeves);
trait.setVisible(Layer.RIGHT_SLEEVE, hasSleeves);
}
if (args.hasValueFlag("pants")) {
boolean hasPants = Boolean.valueOf(args.getFlag("pants"));
trait.setVisible(Layer.LEFT_PANTS, hasPants);
trait.setVisible(Layer.RIGHT_PANTS, hasPants);
}
Messaging.sendTr(sender, Messages.SKIN_LAYERS_SET, npc.getName(),
trait.isVisible(Layer.CAPE), trait.isVisible(Layer.HAT), trait.isVisible(Layer.JACKET),
trait.isVisible(Layer.LEFT_SLEEVE) || trait.isVisible(Layer.RIGHT_SLEEVE),
trait.isVisible(Layer.LEFT_PANTS) || trait.isVisible(Layer.RIGHT_PANTS));
}
@Command(
aliases = { "npc" },
usage = "size [size]",

View File

@ -31,6 +31,7 @@ import net.citizensnpcs.trait.Powered;
import net.citizensnpcs.trait.RabbitType;
import net.citizensnpcs.trait.Saddle;
import net.citizensnpcs.trait.SheepTrait;
import net.citizensnpcs.trait.SkinLayers;
import net.citizensnpcs.trait.SlimeSize;
import net.citizensnpcs.trait.VillagerProfession;
import net.citizensnpcs.trait.WolfModifiers;
@ -65,6 +66,7 @@ public class CitizensTraitFactory implements TraitFactory {
registerTrait(TraitInfo.create(RabbitType.class).withName("rabbittype"));
registerTrait(TraitInfo.create(Saddle.class).withName("saddle"));
registerTrait(TraitInfo.create(SheepTrait.class).withName("sheeptrait"));
registerTrait(TraitInfo.create(SkinLayers.class).withName("skinlayers"));
registerTrait(TraitInfo.create(NPCSkeletonType.class).withName("skeletontype"));
registerTrait(TraitInfo.create(SlimeSize.class).withName("slimesize"));
registerTrait(TraitInfo.create(Spawned.class).withName("spawned"));

View File

@ -235,6 +235,8 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder, Skinnable
controllerMove = new PlayerControllerMove(this);
navigation = new PlayerNavigation(this, world);
NMS.setStepHeight(this, 1); // the default (0) breaks step climbing
setSkinFlags((byte)0xFF);
}
public boolean isNavigating() {
@ -278,6 +280,18 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder, Skinnable
controllerJump.a();
}
@Override
public void setSkinFlags(byte flags) {
// set skin flag byte (DataWatcher API is lacking so
// catch the NPE as a sign that this is a MC 1.7 server without the
// skin flag)
try {
getDataWatcher().watch(10, flags);
} catch (NullPointerException e) {
getDataWatcher().a(10, flags);
}
}
@Override
public void setSkinName(String name) {
Preconditions.checkNotNull(name);
@ -333,15 +347,6 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder, Skinnable
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
// skin flag)
try {
datawatcher.watch(10, Byte.valueOf((byte) 127));
} catch (NullPointerException e) {
datawatcher.a(10, Byte.valueOf((byte) 127));
}
Location current = getBukkitEntity().getLocation(packetLocationCache);
Packet<?>[] packets = new Packet[navigating ? 5 : 6];
if (!navigating) {
@ -422,6 +427,11 @@ public class EntityHumanNPC extends EntityPlayer implements NPCHolder, Skinnable
cserver.getEntityMetadata().setMetadata(this, metadataKey, newMetadataValue);
}
@Override
public void setSkinFlags(byte flags) {
((SkinnableEntity) this.entity).setSkinFlags(flags);
}
@Override
public void setSkinName(String name) {
((SkinnableEntity) this.entity).setSkinName(name);

View File

@ -129,12 +129,12 @@ public class HumanController extends AbstractEntityController {
@Override
public void remove() {
NMS.removeFromWorld(getBukkitEntity());
SkinnableEntity npc = NMS.getSkinnable(getBukkitEntity());
npc.getSkinTracker().onRemoveNPC();
Player entity = getBukkitEntity();
if (entity != null) {
NMS.removeFromWorld(entity);
SkinnableEntity npc = NMS.getSkinnable(entity);
npc.getSkinTracker().onRemoveNPC();
}
super.remove();
}

View File

@ -5,7 +5,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.citizensnpcs.npc.CitizensNPC;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@ -135,8 +134,8 @@ public class SkinPacketTracker {
continue;
// send packet now and later to ensure removal from player list
NMS.sendPlayerListRemove(player, entity.getBukkitEntity());
PLAYER_LIST_REMOVER.sendPacket(player, entity);
NMS.sendTabListRemove(player, entity.getBukkitEntity());
TAB_LIST_REMOVER.sendPacket(player, entity);
}
}
@ -147,27 +146,24 @@ public class SkinPacketTracker {
entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
if (shouldRemoveFromPlayerList()) {
PLAYER_LIST_REMOVER.sendPacket(entry.player, entity);
if (shouldRemoveFromTabList()) {
TAB_LIST_REMOVER.sendPacket(entry.player, entity);
}
}
}, PACKET_DELAY_REMOVE);
}
private void scheduleRemovePacket(PlayerEntry entry, int count) {
if (!shouldRemoveFromPlayerList())
if (!shouldRemoveFromTabList())
return;
entry.removeCount = count;
scheduleRemovePacket(entry);
}
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 boolean shouldRemoveFromTabList() {
return entity.getNPC().data().get("removefromtablist",
Settings.Setting.DISABLE_TABLIST.asBoolean());
}
/**
@ -216,11 +212,11 @@ public class SkinPacketTracker {
entry = new PlayerEntry(player);
}
PLAYER_LIST_REMOVER.cancelPackets(player, entity);
TAB_LIST_REMOVER.cancelPackets(player, entity);
inProgress.put(player.getUniqueId(), entry);
skin.apply(entity);
NMS.sendPlayerListAdd(player, entity.getBukkitEntity());
NMS.sendTabListAdd(player, entity.getBukkitEntity());
scheduleRemovePacket(entry, 2);
}
@ -248,12 +244,12 @@ public class SkinPacketTracker {
private void onPlayerQuit(PlayerQuitEvent event) {
// this also causes any entries in the "inProgress" field to
// be removed.
PLAYER_LIST_REMOVER.cancelPackets(event.getPlayer());
TAB_LIST_REMOVER.cancelPackets(event.getPlayer());
}
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static PlayerListener LISTENER;
private static final int PACKET_DELAY_REMOVE = 1;
private static final PlayerListRemover PLAYER_LIST_REMOVER = new PlayerListRemover();
private static final TabListRemover TAB_LIST_REMOVER = new TabListRemover();
}

View File

@ -0,0 +1,460 @@
package net.citizensnpcs.npc.skin;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.npc.NPCRegistry;
import net.citizensnpcs.util.NMS;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
/**
* Tracks skin updates for players.
*
* @see net.citizensnpcs.EventListen
*/
public class SkinUpdateTracker {
private final Map<SkinnableEntity, Void> navigating = new WeakHashMap<SkinnableEntity, Void>(25);
private final NPCRegistry npcRegistry;
private final Map<String, NPCRegistry> registries;
private final Map<UUID, PlayerTracker> playerTrackers =
new HashMap<UUID, PlayerTracker>(Bukkit.getMaxPlayers() / 2);
private final NPCNavigationUpdater updater = new NPCNavigationUpdater();
/**
* Constructor.
*
* @param npcRegistry
* The primary citizens registry.
* @param registries
* Map of other registries.
*/
public SkinUpdateTracker(NPCRegistry npcRegistry, Map<String, NPCRegistry> registries) {
Preconditions.checkNotNull(npcRegistry);
Preconditions.checkNotNull(registries);
this.npcRegistry = npcRegistry;
this.registries = registries;
updater.runTaskTimer(CitizensAPI.getPlugin(), 1, 1);
new NPCNavigationTracker().runTaskTimer(CitizensAPI.getPlugin(), 3, 7);
}
/**
* Update a player with skin related packets from nearby skinnable NPC's.
*
* @param player
* The player to update.
* @param delay
* The delay before sending the packets.
* @param reset
* True to hard reset the players tracking info, otherwise false.
*/
public void updatePlayer(final Player player, long delay, final boolean reset) {
if (player.hasMetadata("NPC"))
return;
new BukkitRunnable() {
@Override
public void run() {
List<SkinnableEntity> visible = getNearbyNPCs(player, reset, false);
for (SkinnableEntity skinnable : visible) {
skinnable.getSkinTracker().updateViewer(player);
}
}
}.runTaskLater(CitizensAPI.getPlugin(), delay);
}
/**
* Remove a player from the tracker.
*
* <p>
* Used when the player logs out.
* </p>
*
* @param playerId
* The ID of the player.
*/
public void removePlayer(UUID playerId) {
Preconditions.checkNotNull(playerId);
playerTrackers.remove(playerId);
}
/**
* Reset all players currently being tracked.
*
* <p>
* Used when Citizens is reloaded.
* </p>
*/
public void reset() {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasMetadata("NPC"))
continue;
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
if (tracker == null)
continue;
tracker.hardReset(player);
}
}
/**
* Invoke when an NPC is spawned.
*
* @param npc
* The spawned NPC.
*/
public void onNPCSpawn(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
// reset nearby players in case they are not looking at the NPC when it spawns.
resetNearbyPlayers(skinnable);
}
/**
* Invoke when an NPC is despawned.
*
* @param npc
* The despawned NPC.
*/
public void onNPCDespawn(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
navigating.remove(skinnable);
for (PlayerTracker tracker : playerTrackers.values()) {
tracker.fovVisibleSkins.remove(skinnable);
}
}
/**
* Invoke when an NPC begins navigating.
*
* @param npc
* The navigating NPC.
*/
public void onNPCNavigationBegin(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
navigating.put(skinnable, null);
}
/**
* Invoke when an NPC finishes navigating.
*
* @param npc
* The finished NPC.
*/
public void onNPCNavigationComplete(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
navigating.remove(skinnable);
}
/**
* Invoke when a player moves.
*
* @param player
* The player that moved.
*/
public void onPlayerMove(Player player) {
Preconditions.checkNotNull(player);
PlayerTracker updateTracker = playerTrackers.get(player.getUniqueId());
if (updateTracker == null)
return;
if (!updateTracker.shouldUpdate(player))
return;
updatePlayer(player, 10, false);
}
// hard reset players near a skinnable NPC
private void resetNearbyPlayers(SkinnableEntity skinnable) {
Entity entity = skinnable.getBukkitEntity();
if (entity == null || !entity.isValid())
return;
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
viewDistance *= viewDistance;
Location location = entity.getLocation(NPC_LOCATION);
List<Player> players = entity.getWorld().getPlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
double distanceSquared = player.getLocation(CACHE_LOCATION).distanceSquared(location);
if (distanceSquared > viewDistance)
continue;
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
if (tracker != null) {
tracker.hardReset(player);
}
}
}
private List<SkinnableEntity> getNearbyNPCs(Player player, boolean reset, boolean checkFov) {
List<SkinnableEntity> results = new ArrayList<SkinnableEntity>();
PlayerTracker tracker = getTracker(player, reset);
for (NPC npc : getAllNPCs()) {
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
continue;
// if checking field of view, don't add skins that have already been updated for FOV
if (checkFov && tracker.fovVisibleSkins.contains(skinnable))
continue;
if (canSee(player, skinnable, checkFov)) {
results.add(skinnable);
}
}
return results;
}
private Iterable<NPC> getAllNPCs() {
return Iterables.filter(Iterables.concat(npcRegistry, Iterables.concat(registries.values())),
Predicates.notNull());
}
// get all navigating skinnable NPC's within the players FOV that have not been "seen" yet
private void getNewVisibleNavigating(Player player, Collection<SkinnableEntity> output) {
PlayerTracker tracker = getTracker(player, false);
for (SkinnableEntity skinnable : navigating.keySet()) {
// make sure player hasn't already been updated to prevent excessive tab list flashing
// while NPC's are navigating and to reduce the number of times #canSee is invoked.
if (tracker.fovVisibleSkins.contains(skinnable))
continue;
if (canSee(player, skinnable, true))
output.add(skinnable);
}
}
@Nullable
private SkinnableEntity getSkinnable(NPC npc) {
Entity entity = npc.getEntity();
if (entity == null)
return null;
return NMS.getSkinnable(entity);
}
// get a players tracker, create new one if not exists.
private PlayerTracker getTracker(Player player, boolean reset) {
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
if (tracker == null) {
tracker = new PlayerTracker(player);
playerTrackers.put(player.getUniqueId(), tracker);
}
else if (reset) {
tracker.hardReset(player);
}
return tracker;
}
// determines if a player is near a skinnable entity and, if checkFov set, if the
// skinnable entity is within the players field of view.
private boolean canSee(Player player, SkinnableEntity skinnable, boolean checkFov) {
Player entity = skinnable.getBukkitEntity();
if (entity == null)
return false;
if (!player.canSee(entity))
return false;
if (!player.getWorld().equals(entity.getWorld()))
return false;
Location playerLoc = player.getLocation(CACHE_LOCATION);
Location skinLoc = entity.getLocation(NPC_LOCATION);
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
viewDistance *= viewDistance;
if (playerLoc.distanceSquared(skinLoc) > viewDistance)
return false;
// see if the NPC is within the players field of view
if (checkFov) {
double deltaX = skinLoc.getX() - playerLoc.getX();
double deltaZ = skinLoc.getZ() - playerLoc.getZ();
double angle = Math.atan2(deltaX, deltaZ);
float skinYaw = NMS.clampYaw(-(float) Math.toDegrees(angle));
float playerYaw = NMS.clampYaw(playerLoc.getYaw());
float upperBound = playerYaw + FIELD_OF_VIEW;
float lowerBound = playerYaw - FIELD_OF_VIEW;
return skinYaw >= lowerBound && skinYaw <= upperBound;
}
return true;
}
// Tracks player location and yaw to determine when the player should be updated
// with nearby skins.
private class PlayerTracker {
final Location location = new Location(null, 0, 0, 0);
final Set<SkinnableEntity> fovVisibleSkins = new HashSet<SkinnableEntity>(20);
int rotationCount;
boolean hasMoved;
float upperBound;
float lowerBound;
PlayerTracker(Player player) {
hardReset(player);
}
boolean shouldUpdate(Player player) {
Location currentLoc = player.getLocation(CACHE_LOCATION);
if (!hasMoved) {
hasMoved = true;
return true;
}
if (rotationCount < 3) {
float yaw = NMS.clampYaw(currentLoc.getYaw());
boolean hasRotated = yaw < lowerBound || yaw > upperBound;
// update the first 3 times the player rotates. helps load skins around player
// after the player logs/teleports.
if (hasRotated) {
rotationCount++;
reset(player);
return true;
}
}
// make sure player is in same world
if (!currentLoc.getWorld().equals(this.location.getWorld())) {
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(this.location);
if (rotationCount < 3) {
float rotationDegrees = Settings.Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat();
float yaw = NMS.clampYaw(this.location.getYaw());
this.upperBound = yaw + rotationDegrees;
this.lowerBound = yaw - rotationDegrees;
}
}
// reset all
void hardReset(Player player) {
this.hasMoved = false;
this.rotationCount = 0;
this.fovVisibleSkins.clear();
reset(player);
}
}
// update players when the NPC navigates into their field of view
private class NPCNavigationTracker extends BukkitRunnable {
@Override
public void run() {
if (navigating.isEmpty() || playerTrackers.isEmpty())
return;
List<SkinnableEntity> nearby = new ArrayList<SkinnableEntity>(10);
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
getNewVisibleNavigating(player, nearby);
for (SkinnableEntity skinnable : nearby) {
PlayerTracker tracker = getTracker(player, false);
tracker.fovVisibleSkins.add(skinnable);
updater.queue.offer(new UpdateInfo(player, skinnable));
}
nearby.clear();
}
}
}
// Updates players. Repeating task used to schedule updates without
// causing excessive scheduling.
private class NPCNavigationUpdater extends BukkitRunnable {
Queue<UpdateInfo> queue = new ArrayDeque<UpdateInfo>(20);
@Override
public void run() {
while (!queue.isEmpty()) {
UpdateInfo info = queue.remove();
info.entity.getSkinTracker().updateViewer(info.player);
}
}
}
private static class UpdateInfo {
Player player;
SkinnableEntity entity;
UpdateInfo(Player player, SkinnableEntity entity) {
this.player = player;
this.entity = entity;
}
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
private static final int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50;
private static final float FIELD_OF_VIEW = 70f;
}

View File

@ -9,11 +9,6 @@ import org.bukkit.entity.Player;
*/
public interface SkinnableEntity extends NPCHolder {
/**
* Get the entities skin packet tracker.
*/
SkinPacketTracker getSkinTracker();
/**
* Get the bukkit entity.
*/
@ -29,12 +24,33 @@ public interface SkinnableEntity extends NPCHolder {
*/
String getSkinName();
/**
* Get the entities skin packet tracker.
*/
SkinPacketTracker getSkinTracker();
/**
* Set the bit flags that represent the skin layer parts visibility.
*
* <p>
* Setting the skin flags automatically updates the NPC skin.
* </p>
*
* @param flags
* The bit flags.
*/
void setSkinFlags(byte flags);
/**
* Set the name of the player whose skin the NPC
* uses.
*
* <p>Setting the skin name automatically updates and
* respawn the NPC.</p>
* <p>
* Setting the skin name automatically updates and respawn the NPC.
* </p>
*
* @param name
* The skin name.
*/
void setSkinName(String name);
}

View File

@ -25,10 +25,10 @@ import net.citizensnpcs.util.NMS;
* Collects entities to remove and sends them all to the player in a single packet.
* </p>
*/
public class PlayerListRemover {
public class TabListRemover {
private final Map<UUID, PlayerEntry> pending = new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
PlayerListRemover() {
TabListRemover() {
Bukkit.getScheduler().runTaskTimer(CitizensAPI.getPlugin(), new Sender(), 2, 2);
}
@ -143,7 +143,7 @@ public class PlayerListRemover {
}
if (entry.player.isOnline())
NMS.sendPlayerListRemove(entry.player, skinnableList);
NMS.sendTabListRemove(entry.player, skinnableList);
// notify skin trackers that a remove packet has been sent to a player
for (SkinnableEntity entity : skinnableList) {

View File

@ -0,0 +1,262 @@
package net.citizensnpcs.trait;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.npc.skin.SkinnableEntity;
import net.citizensnpcs.util.NMS;
public class SkinLayers extends Trait {
@Persist("cape")
private boolean cape = true;
@Persist("hat")
private boolean hat = true;
@Persist("jacket")
private boolean jacket = true;
@Persist("left-pants")
private boolean leftPants = true;
@Persist("left-sleeve")
private boolean leftSleeve = true;
@Persist("right-pants")
private boolean rightPants = true;
@Persist("right-sleeve")
private boolean rightSleeve = true;
public SkinLayers() {
super("skinlayers");
}
public SkinLayers show() {
cape = true;
hat = true;
jacket = true;
leftSleeve = true;
rightSleeve = true;
leftPants = true;
rightPants = true;
setFlags();
return this;
}
public SkinLayers showCape() {
cape = true;
setFlags();
return this;
}
public SkinLayers showHat() {
hat = true;
setFlags();
return this;
}
public SkinLayers showJacket() {
jacket = true;
setFlags();
return this;
}
public SkinLayers showLeftPants() {
leftPants = true;
setFlags();
return this;
}
public SkinLayers showLeftSleeve() {
leftSleeve = true;
setFlags();
return this;
}
public SkinLayers showPants() {
leftPants = true;
rightPants = true;
setFlags();
return this;
}
public SkinLayers showRightPants() {
rightPants = true;
setFlags();
return this;
}
public SkinLayers showRightSleeve() {
rightSleeve = true;
setFlags();
return this;
}
public SkinLayers showSleeves() {
leftSleeve = true;
rightSleeve = true;
setFlags();
return this;
}
public SkinLayers hide() {
cape = false;
hat = false;
jacket = false;
leftSleeve = false;
rightSleeve = false;
leftPants = false;
rightPants = false;
setFlags();
return this;
}
public SkinLayers hideCape() {
cape = false;
setFlags();
return this;
}
public SkinLayers hideHat() {
hat = false;
setFlags();
return this;
}
public SkinLayers hideJacket() {
jacket = false;
setFlags();
return this;
}
public SkinLayers hideLeftPants() {
leftPants = false;
setFlags();
return this;
}
public SkinLayers hideLeftSleeve() {
leftSleeve = false;
setFlags();
return this;
}
public SkinLayers hidePants() {
leftPants = false;
rightPants = false;
setFlags();
return this;
}
public SkinLayers hideRightPants() {
rightPants = false;
setFlags();
return this;
}
public SkinLayers hideRightSleeve() {
rightSleeve = false;
setFlags();
return this;
}
public SkinLayers hideSleeves() {
leftSleeve = false;
rightSleeve = false;
setFlags();
return this;
}
public boolean isVisible(Layer layer) {
switch (layer) {
case CAPE:
return cape;
case JACKET:
return jacket;
case LEFT_SLEEVE:
return leftSleeve;
case RIGHT_SLEEVE:
return rightSleeve;
case LEFT_PANTS:
return leftPants;
case RIGHT_PANTS:
return rightPants;
case HAT:
return hat;
default:
return false;
}
}
@Override
public void onAttach() {
setFlags();
}
@Override
public void onSpawn() {
setFlags();
}
public SkinLayers setVisible(Layer layer, boolean isVisible) {
switch (layer) {
case CAPE:
cape = isVisible;
break;
case JACKET:
jacket = isVisible;
break;
case LEFT_SLEEVE:
leftSleeve = isVisible;
break;
case RIGHT_SLEEVE:
rightSleeve = isVisible;
break;
case LEFT_PANTS:
leftPants = isVisible;
break;
case RIGHT_PANTS:
rightPants = isVisible;
break;
case HAT:
hat = isVisible;
break;
}
setFlags();
return this;
}
@Override
public String toString() {
return "SkinLayers{cape:" + cape + ", hat:" + hat + ", jacket:" + jacket + ", leftSleeve:" + leftSleeve
+ ", rightSleeve:" + rightSleeve + ", leftPants:" + leftPants + ", rightPants:" + rightPants + "}";
}
private void setFlags() {
if (!npc.isSpawned())
return;
SkinnableEntity skinnable = NMS.getSkinnable(npc.getEntity());
if (skinnable == null)
return;
int flags = 0xFF;
for (Layer layer : Layer.values()) {
if (!isVisible(layer)) {
flags &= ~layer.flag;
}
}
skinnable.setSkinFlags((byte)flags);
}
public enum Layer {
CAPE (0),
JACKET (1),
LEFT_SLEEVE (2),
RIGHT_SLEEVE (3),
LEFT_PANTS (4),
RIGHT_PANTS (5),
HAT (6);
final int flag;
Layer(int offset) {
this.flag = 1 << offset;
}
}
}

View File

@ -192,6 +192,7 @@ public class Messages {
public static final String SIZE_SET = "citizens.commands.npc.size.set";
public static final String SKELETON_TYPE_SET = "citizens.commands.npc.skeletontype.set";
public static final String SKIN_CLEARED = "citizens.commands.npc.skin.cleared";
public static final String SKIN_LAYERS_SET = "citizens.commands.npc.skin.layers-set";
public static final String SKIN_SET = "citizens.commands.npc.skin.set";
public static final String SKIPPING_BROKEN_TRAIT = "citizens.notifications.skipping-broken-trait";
public static final String SKIPPING_INVALID_ANCHOR = "citizens.notifications.skipping-invalid-anchor";

View File

@ -117,7 +117,7 @@ public class NMS {
return null;
}
public static void sendPlayerListAdd(Player recipient, Player listPlayer) {
public static void sendTabListAdd(Player recipient, Player listPlayer) {
Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(listPlayer);
@ -127,7 +127,7 @@ public class NMS {
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, entity));
}
public static void sendPlayerListRemove(Player recipient, Player listPlayer) {
public static void sendTabListRemove(Player recipient, Player listPlayer) {
Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(listPlayer);
@ -137,8 +137,8 @@ public class NMS {
PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, entity));
}
public static void sendPlayerListRemove(Player recipient,
Collection<? extends SkinnableEntity> skinnableNPCs) {
public static void sendTabListRemove(Player recipient,
Collection<? extends SkinnableEntity> skinnableNPCs) {
Preconditions.checkNotNull(recipient);
Preconditions.checkNotNull(skinnableNPCs);

View File

@ -96,6 +96,7 @@ citizens.commands.npc.respawn.describe=Respawn delay is currently [[{0}]].
citizens.commands.npc.select.already-selected=You already have that NPC selected.
citizens.commands.npc.skin.set=[[{0}]]''s skin name set to [[{1}]].
citizens.commands.npc.skin.cleared=[[{0}]]''s skin name was cleared.
citizens.commands.npc.skin.layers-set=[[{0}]]''s skin layers: cape - [[{1}]], hat - [[{2}]], jacket - [[{3}]], sleeves - [[{4}]], pants - [[{5}]].
citizens.commands.npc.size.description={0}''s size is [[{1}]].
citizens.commands.npc.size.set={0}''s size set to [[{1}]].
citizens.commands.npc.sound.invalid-sound=Invalid sound.

View File

@ -646,6 +646,7 @@ permissions:
citizens.npc.size: true
citizens.npc.skeletontype: true
citizens.npc.skin: true
citizens.npc.skinlayers: true
citizens.npc.sound: true
citizens.npc.spawn: true
citizens.npc.speak: true
@ -994,6 +995,7 @@ permissions:
citizens.npc.size: true
citizens.npc.skeletontype: true
citizens.npc.skin: true
citizens.npc.skinlayers: true
citizens.npc.sound: true
citizens.npc.spawn: true
citizens.npc.speak: true