package com.bergerkiller.bukkit.common.entity;
import java.util.List;
import java.util.ListIterator;
import java.util.logging.Level;
import net.minecraft.server.Chunk;
import net.minecraft.server.Entity;
import net.minecraft.server.EntityTrackerEntry;
import net.minecraft.server.IInventory;
import net.minecraft.server.World;
import org.bukkit.Location;
import org.bukkit.craftbukkit.entity.CraftEntity;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Item;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import com.bergerkiller.bukkit.common.bases.ExtendedEntity;
import com.bergerkiller.bukkit.common.controller.DefaultEntityController;
import com.bergerkiller.bukkit.common.controller.DefaultEntityNetworkController;
import com.bergerkiller.bukkit.common.controller.EntityController;
import com.bergerkiller.bukkit.common.controller.EntityNetworkController;
import com.bergerkiller.bukkit.common.controller.ExternalEntityNetworkController;
import com.bergerkiller.bukkit.common.conversion.Conversion;
import com.bergerkiller.bukkit.common.entity.nms.NMSEntityHook;
import com.bergerkiller.bukkit.common.entity.nms.NMSEntityTrackerEntry;
import com.bergerkiller.bukkit.common.entity.type.CommonItem;
import com.bergerkiller.bukkit.common.entity.type.CommonLivingEntity;
import com.bergerkiller.bukkit.common.entity.type.CommonPlayer;
import com.bergerkiller.bukkit.common.internal.CommonNMS;
import com.bergerkiller.bukkit.common.internal.CommonPlugin;
import com.bergerkiller.bukkit.common.reflection.SafeField;
import com.bergerkiller.bukkit.common.reflection.classes.EntityPlayerRef;
import com.bergerkiller.bukkit.common.reflection.classes.EntityRef;
import com.bergerkiller.bukkit.common.reflection.classes.EntityTrackerEntryRef;
import com.bergerkiller.bukkit.common.reflection.classes.PlayerConnectionRef;
import com.bergerkiller.bukkit.common.reflection.classes.WorldRef;
import com.bergerkiller.bukkit.common.reflection.classes.WorldServerRef;
import com.bergerkiller.bukkit.common.utils.CommonUtil;
import com.bergerkiller.bukkit.common.utils.EntityUtil;
import com.bergerkiller.bukkit.common.utils.WorldUtil;
import com.bergerkiller.bukkit.common.wrappers.EntityTracker;
import com.bergerkiller.bukkit.common.wrappers.IntHashMap;
/**
* Wrapper class for additional methods Bukkit can't or doesn't provide.
*
* @param <T> - type of Entity
*/
public class CommonEntity<T extends org.bukkit.entity.Entity> extends ExtendedEntity<T> {
public CommonEntity(T entity) {
super(entity);
}
/**
* Gets the Entity Network Controller currently assigned to this Entity.
* If none is available, this method returns Null.
* If no custom network controller is set, this method returns a new
* {@link DefaultEntityNetworkController} instance.
*
* @return Entity Network Controller, or null if not available
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public EntityNetworkController<CommonEntity<T>> getNetworkController() {
if (EntityRef.world.getInternal(getHandle()) == null) {
return null;
}
final EntityNetworkController result;
final Object entityTrackerEntry = WorldUtil.getTrackerEntry(entity);
if (entityTrackerEntry == null) {
return null;
} else if (entityTrackerEntry instanceof NMSEntityTrackerEntry) {
result = ((NMSEntityTrackerEntry) entityTrackerEntry).getController();
} else if (EntityTrackerEntry.class.equals(entityTrackerEntry.getClass())) {
result = new DefaultEntityNetworkController();
result.bind(this, entityTrackerEntry);
} else {
result = new ExternalEntityNetworkController();
result.bind(this, entityTrackerEntry);
}
return result;
}
/**
* Sets an Entity Network Controller for this Entity.
* To stop tracking this minecart, pass in Null.
* To default back to the net.minecraft.server implementation, pass in a new
* {@link DefaultEntityNetworkController} instance.<br>
* <br>
* This method only works if the Entity world has previously been set.
*
* @param controller to set to
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public void setNetworkController(EntityNetworkController controller) {
if (getWorld() == null) {
throw new RuntimeException("Can not set the network controller when no world is known! (need to spawn it?)");
}
final EntityTracker tracker = WorldUtil.getTracker(getWorld());
final Object storedEntry = tracker.getEntry(entity);
// Properly handle a previously set controller
if (storedEntry instanceof NMSEntityTrackerEntry) {
final EntityNetworkController oldController = ((NMSEntityTrackerEntry) storedEntry).getController();
if (oldController == controller) {
return;
} else if (oldController != null) {
oldController.onDetached();
}
}
// Take care of null controllers - stop tracking
if (controller == null) {
tracker.stopTracking(entity);
return;
}
final Object newEntry;
if (controller instanceof DefaultEntityNetworkController) {
// Assign the default Entity Tracker Entry
if (EntityTrackerEntryRef.TEMPLATE.isType(storedEntry)) {
// Nothing to be done here
newEntry = storedEntry;
} else {
// Create a new entry
final CommonEntityType type = CommonEntityType.byEntity(entity);
newEntry = new EntityTrackerEntry(getHandle(Entity.class), type.networkViewDistance, type.networkUpdateInterval, type.networkIsMobile);
// Transfer data if needed
if (storedEntry != null) {
EntityTrackerEntryRef.TEMPLATE.transfer(storedEntry, newEntry);
}
}
} else if (controller instanceof ExternalEntityNetworkController) {
// Use the entry as stored by the external network controller
newEntry = controller.getHandle();
// Be sure to refresh stats using the old entry
if (storedEntry != null && newEntry != null) {
EntityTrackerEntryRef.TEMPLATE.transfer(storedEntry, newEntry);
}
} else {
// Assign a new Entity Tracker Entry with controller capabilities
if (storedEntry instanceof NMSEntityTrackerEntry) {
// Use the previous entry - hotswap the controller
newEntry = storedEntry;
EntityTrackerEntryRef.viewers.get(newEntry).clear();
} else {
// Create a new entry from scratch
newEntry = new NMSEntityTrackerEntry(this.getEntity());
// Transfer possible information over
if (storedEntry != null) {
EntityTrackerEntryRef.TEMPLATE.transfer(storedEntry, newEntry);
}
}
}
// Attach the entry to the controller
controller.bind(this, newEntry);
// Attach (new?) entry to the world
if (storedEntry != newEntry) {
tracker.setEntry(entity, newEntry);
// Make sure to update the viewers
EntityTrackerEntryRef.scanPlayers(newEntry, getWorld().getPlayers());
}
}
/**
* Gets the Entity Controller currently assigned to this Entity.
* If no custom controller is set, this method returns a new
* {@link DefaultEntityController} instance.
*
* @return Entity Controller
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public EntityController<CommonEntity<T>> getController() {
if (isHooked()) {
return (EntityController<CommonEntity<T>>) getHandle(NMSEntityHook.class).getController();
}
final EntityController controller = new DefaultEntityController();
controller.bind(this);
return controller;
}
/**
* Checks whether this particular Entity supports the use of Entity Controllers.
* If this method returns True, {@link #setController(EntityController)} can be used.<br><br>
*
* Note that Entity Network Controllers are always supported.
*
* @return True if Entity Controllers are supported, False if not
*/
public boolean hasControllerSupport() {
// Check whether already hooked
if (isHooked()) {
return true;
}
final Object handle = getHandle();
final CommonEntityType type = CommonEntityType.byNMSEntity(handle);
// Check whether the handle is not of an external-plugin type
if (handle == null || !type.nmsType.isType(handle)) {
return false;
}
// Check whether the CommonEntityType supports hooking
return type.hasNMSEntity();
}
/**
* Checks whether the Entity is a BKCommonLib hook
*
* @return True if hooked, False if not
*/
protected boolean isHooked() {
return getHandle() instanceof NMSEntityHook;
}
/**
* Replaces the current entity, if needed, with the BKCommonLib Hook entity type
*/
protected void prepareHook() {
final Entity oldInstance = getHandle(Entity.class);
if (oldInstance instanceof NMSEntityHook) {
// Already hooked
return;
}
// Check whether conversion is allowed
final String oldInstanceName = oldInstance.getClass().getName();
final CommonEntityType type = CommonEntityType.byEntity(entity);
if (!type.nmsType.isType(oldInstance)) {
throw new RuntimeException("Can not assign controllers to a custom Entity Type (" + oldInstanceName + ")");
}
if (!type.hasNMSEntity()) {
throw new RuntimeException("Entity of type '" + type.entityType + "' has no Controller support!");
}
// Respawn the entity and attach the controller
try {
// Create a new entity instance and perform data/property transfer
Object newInstance = type.createNMSHookEntity(this);
type.nmsType.transfer(oldInstance, newInstance);
replaceEntity((Entity) newInstance);
} catch (Throwable t) {
throw new RuntimeException("Failed to set controller:", t);
}
}
private void replaceEntity(final Entity newInstance) {
final Entity oldInstance = getHandle(Entity.class);
oldInstance.dead = true;
newInstance.dead = false;
oldInstance.valid = false;
newInstance.valid = true;
// *** Bukkit Entity ***
((CraftEntity) entity).setHandle(newInstance);
if (entity instanceof InventoryHolder) {
Inventory inv = ((InventoryHolder) entity).getInventory();
if (inv instanceof CraftInventory && newInstance instanceof IInventory) {
SafeField.set(inv, "inventory", newInstance);
}
}
// *** Give the old entity a new Bukkit Entity ***
EntityRef.bukkitEntity.set(oldInstance, EntityRef.createEntity(oldInstance));
// *** Passenger/Vehicle ***
if (newInstance.vehicle != null) {
newInstance.vehicle.passenger = newInstance;
}
if (newInstance.passenger != null) {
newInstance.passenger.vehicle = newInstance;
}
// Only do this replacement logic for Entities that are already spawned
if (this.isSpawned()) {
// Now proceed to replace this NMS Entity in all places imaginable.
// First load the chunk so we can at least work on something
Chunk chunk = CommonNMS.getNative(getWorld().getChunkAt(getChunkX(), getChunkZ()));
// *** Entities By ID Map ***
final IntHashMap<Object> entitiesById = WorldServerRef.entitiesById.get(oldInstance.world);
if (entitiesById.remove(oldInstance.getId()) == null) {
CommonUtil.nextTick(new Runnable() {
public void run() {
entitiesById.put(newInstance.getId(), newInstance);
}
});
}
entitiesById.put(newInstance.getId(), newInstance);
// *** EntityTrackerEntry ***
final EntityTracker tracker = WorldUtil.getTracker(getWorld());
Object entry = tracker.getEntry(entity);
if (entry != null) {
EntityTrackerEntryRef.tracker.set(entry, entity);
}
if (hasPassenger()) {
entry = tracker.getEntry(getPassenger());
if (entry != null) {
EntityTrackerEntryRef.vehicle.set(entry, entity);
}
}
// *** World ***
replaceInList(oldInstance.world.entityList, newInstance);
replaceInList(WorldRef.entityRemovalList.get(oldInstance.world), newInstance);
// *** Chunk ***
final int chunkY = getChunkY();
if (!replaceInChunk(chunk, chunkY, newInstance)) {
for (int y = 0; y < chunk.entitySlices.length; y++) {
if (y != chunkY && replaceInChunk(chunk, y, newInstance)) {
break;
}
}
}
}
}
private static boolean replaceInChunk(Chunk chunk, int chunkY, Entity entity) {
if (replaceInList(chunk.entitySlices[chunkY], entity)) {
chunk.m = true;
return true;
} else {
return false;
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static boolean replaceInList(List list, Entity entity) {
ListIterator<Entity> iter = list.listIterator();
while (iter.hasNext()) {
if (iter.next().getId() == entity.getId()) {
iter.set(entity);
return true;
}
}
return false;
}
/**
* Sets an Entity Controller for this Entity.
* This method throws an Exception if this kind of Entity is not supported.
*
* @param controller to set to
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public void setController(EntityController controller) {
// Prepare the hook
this.prepareHook();
// If null, resolve to the default type
if (controller == null) {
controller = new DefaultEntityController();
}
getController().bind(null);
controller.bind(this);
}
/**
* Performs all the logic normally performed after ticking a single Entity.
* If onTick is managed externally, this method keeps it compatible.
* Calling this method results in the entity properly moved between chunks, and other logic.
*/
public void doPostTick() {
final int oldcx = getChunkX();
final int oldcy = getChunkY();
final int oldcz = getChunkZ();
final int newcx = loc.x.chunk();
final int newcy = loc.y.chunk();
final int newcz = loc.z.chunk();
final org.bukkit.World world = getWorld();
final boolean changedChunks = oldcx != newcx || oldcy != newcy || oldcz != newcz;
boolean isLoaded = this.isInLoadedChunk();
// Handle chunk/slice movement
// Remove from the previous chunk
if (isLoaded && changedChunks) {
final org.bukkit.Chunk chunk = WorldUtil.getChunk(world, oldcx, oldcz);
if (chunk != null) {
WorldUtil.removeEntity(chunk, entity);
}
}
// Add to the new chunk
if (!isLoaded || changedChunks) {
final org.bukkit.Chunk chunk = WorldUtil.getChunk(world, newcx, newcz);
if (isLoaded = chunk != null) {
WorldUtil.addEntity(chunk, entity);
}
EntityRef.isLoaded.set(getHandle(), isLoaded);
}
// Tick the passenger
if (isLoaded && hasPassenger()) {
final org.bukkit.entity.Entity passenger = getPassenger();
if (!passenger.isDead() && passenger.getVehicle() == entity) {
CommonEntity<?> commonPassenger = get(passenger);
commonPassenger.getController().onTick();
commonPassenger.doPostTick();
} else {
setPassengerSilent(null);
}
}
}
@Override
public boolean teleport(Location location, TeleportCause cause) {
if (isDead()) {
return false;
}
// Preparations prior to teleportation
final Entity entityHandle = CommonNMS.getNative(entity);
final CommonEntity<?> passenger = get(getPassenger());
final World newworld = CommonNMS.getNative(location.getWorld());
final boolean isWorldChange = entityHandle.world != newworld;
final EntityNetworkController<?> oldNetworkController = getNetworkController();
final boolean hasNetworkController = !(oldNetworkController instanceof DefaultEntityNetworkController);
WorldUtil.loadChunks(location, 3);
// If in a vehicle, make sure we eject first
if (isInsideVehicle()) {
getVehicle().eject();
}
// If vehicle, eject the passenger first
if (hasPassenger()) {
setPassengerSilent(null);
}
// Perform actual teleportation
final boolean succ;
if (!isWorldChange || entity instanceof Player) {
// First: stop tracking the entity
final EntityTracker tracker = WorldUtil.getTracker(getWorld());
tracker.stopTracking(entity);
// Destroy packets are queued: Make sure to send them RIGHT NOW
for (Player bukkitPlayer : WorldUtil.getPlayers(getWorld())) {
CommonPlayer player = get(bukkitPlayer);
if (player != null) {
player.flushEntityRemoveQueue();
}
}
// Teleport
succ = entity.teleport(location, cause);
// Start tracking the entity again
if (!hasNetworkController && !isWorldChange) {
tracker.startTracking(entity);
}
} else {
// Remove from one world and add to the other
entityHandle.world.removeEntity(entityHandle);
entityHandle.dead = false;
entityHandle.world = newworld;
entityHandle.setLocation(location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
entityHandle.world.addEntity(entityHandle);
succ = true;
}
if (hasNetworkController) {
this.setNetworkController(oldNetworkController);
}
if (!succ) {
return false;
}
// If there was a passenger, teleport it and let passenger enter again
if (passenger != null) {
// Teleport the passenger, but ignore the chunk send check so vehicle is properly spawned to all players
EntityRef.ignoreChunkCheck.set(entityHandle, true);
final boolean passengerTeleported = passenger.teleport(location, cause);
EntityRef.ignoreChunkCheck.set(entityHandle, false);
if (passengerTeleported) {
setPassengerSilent(passenger.getEntity());
// For players, set checkMovement to True - some odd issue
if (passenger instanceof CommonPlayer) {
Object connection = EntityPlayerRef.playerConnection.get(passenger.getHandle());
if (connection != null) {
PlayerConnectionRef.checkMovement.set(connection, true);
}
}
}
}
return true;
}
/**
* Spawns this Entity at the Location and using the network controller specified.
*
* @param location to spawn at
* @param networkController to assign to the Entity after spawning
* @return True if spawning occurred, False if not
* @see #spawn(Location)
*/
@SuppressWarnings("rawtypes")
public final boolean spawn(Location location, EntityNetworkController networkController) {
final boolean spawned = spawn(location);
this.setNetworkController(networkController);
return spawned;
}
/**
* Spawns this Entity at the Location specified.
* Note that if important properties have to be set beforehand, this should be done first.
* It is recommended to set Entity Controllers before spawning, not after.
* This method will trigger Entity spawning events.
* The network controller can ONLY be set after spawning.
* To be on the safe side, use the Network Controller spawn alternative.
*
* @param location to spawn at
* @return True if the Entity spawned, False if not (and just teleported)
*/
public boolean spawn(Location location) {
if (this.isSpawned()) {
teleport(location);
return false;
}
EntityNetworkController<?> controller = getNetworkController();
if (controller != null) {
controller.onAttached();
}
last.set(loc.set(location));
EntityUtil.addEntity(entity);
// Perform controller attaching
getController().onAttached();
getNetworkController().onAttached();
return true;
}
/**
* Obtains a (new) {@link CommonItem} instance providing additional methods for the Item specified.
* This method never returns null, unless the input Entity is null.
*
* @param item to get a CommonItem for
* @return a (new) CommonItem instance for the Item
*/
public static CommonItem get(Item item) {
return get(item, CommonItem.class);
}
/**
* Obtains a (new) {@link CommonPlayer} instance providing additional methods for the Player specified.
* This method never returns null, unless the input Entity is null.
*
* @param player to get a CommonPlayer for
* @return a (new) CommonPlayer instance for the Player
*/
public static CommonPlayer get(Player player) {
return get(player, CommonPlayer.class);
}
/**
* Obtains a (new) {@link CommonLivingEntity} instance providing additional methods for the Living Entity specified.
* This method never returns null, unless the input Entity is null.
*
* @param livingEntity to get a CommonLivingEntity for
* @return a (new) CommonLivingEntity instance for the Living Entity
*/
@SuppressWarnings("unchecked")
public static <T extends LivingEntity, C extends CommonLivingEntity<? extends T>> C get(T livingEntity) {
return (C) get(livingEntity, CommonLivingEntity.class);
}
/**
* Obtains a (new) {@link CommonEntity} instance providing additional methods for the Entity specified.
* A specific CommonEntity extension that best fits the input Entity can be requested using this method.
* This method never returns null, unless the input Entity is null.
*
* @param entity to get a CommonEntity for
* @param type of CommonEntity to get
* @return a (new) CommonEntity type requested, or null if casting failed/entity is invalid
*/
public static <T extends org.bukkit.entity.Entity, C extends CommonEntity<? extends T>> C get(T entity, Class<C> type) {
return CommonUtil.tryCast(get(entity), type);
}
/**
* Obtains a (new) {@link CommonEntity} instance providing additional methods for the Entity specified.
* This method never returns null, unless the input Entity is null.
*
* @param entity to get a CommonEntity for
* @return a (new) CommonEntity instance for the Entity
*/
@SuppressWarnings("unchecked")
public static <T extends org.bukkit.entity.Entity> CommonEntity<T> get(T entity) {
if (entity == null) {
return null;
}
final Object handle = Conversion.toEntityHandle.convert(entity);
if (handle == null) {
return null;
}
if (handle instanceof NMSEntityHook) {
EntityController<?> controller = ((NMSEntityHook) handle).getController();
if (controller != null) {
return (CommonEntity<T>) controller.getEntity();
}
}
return CommonEntityType.byNMSEntity(handle).createCommonEntity(entity);
}
/**
* Creates (but does not spawn) a new Common Entity backed by a proper Entity.
*
* @param entityType to create
* @return a new CommonEntity type instance
*/
public static CommonEntity<?> create(EntityType entityType) {
CommonEntityType type = CommonEntityType.byEntityType(entityType);
if (type == CommonEntityType.UNKNOWN) {
throw new IllegalArgumentException("The Entity Type '" + entityType + "' is invalid!");
}
final CommonEntity<org.bukkit.entity.Entity> entity = type.createCommonEntity(null);
// Spawn a new NMS Entity
Entity handle;
if (type.hasNMSEntity()) {
handle = (Entity) type.createNMSHookEntity(entity);
} else {
throw new RuntimeException("The Entity Type '" + entityType + "' has no suitable Entity constructor to use!");
}
entity.entity = Conversion.toEntity.convert(handle);
// Create a new CommonEntity and done
return entity;
}
/**
* Clears possible network or Entity controllers from the Entity.
* This should be called when a specific Entity should default back to all default behaviours.
*
* @param entity to clear the controllers of
*/
public static void clearControllers(org.bukkit.entity.Entity entity) {
CommonEntity<?> commonEntity = get(entity);
Object oldInstance = commonEntity.getHandle();
// Detach controller and undo hook Entity replacement
if (oldInstance instanceof NMSEntityHook) {
try {
CommonEntityController<?> controller = ((NMSEntityHook) oldInstance).getController();
if (controller != null) {
controller.onDetached();
}
} catch (Throwable t) {
CommonPlugin.LOGGER.log(Level.SEVERE, "Failed to handle controller detachment:");
t.printStackTrace();
}
try {
CommonEntityType type = CommonEntityType.byNMSEntity(oldInstance);
// Transfer data and replace
Object newInstance = type.createNMSEntity();
type.nmsType.transfer(oldInstance, newInstance);
commonEntity.replaceEntity((Entity) newInstance);
} catch (Throwable t) {
CommonPlugin.LOGGER.log(Level.SEVERE, "Failed to unhook Common Entity Controller:");
t.printStackTrace();
}
}
// Unhook network controller
EntityNetworkController<?> controller = commonEntity.getNetworkController();
if (controller != null && !(controller instanceof DefaultEntityNetworkController)) {
commonEntity.setNetworkController(new DefaultEntityNetworkController());
}
}
}