package com.bergerkiller.bukkit.common.controller;
import java.util.Collection;
import java.util.Collections;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerVelocityEvent;
import org.bukkit.util.Vector;
import net.minecraft.server.AttributeMapServer;
import net.minecraft.server.EntityLiving;
import net.minecraft.server.EntityTrackerEntry;
import net.minecraft.server.MathHelper;
import net.minecraft.server.MobEffect;
import com.bergerkiller.bukkit.common.Common;
import com.bergerkiller.bukkit.common.bases.mutable.IntLocationAbstract;
import com.bergerkiller.bukkit.common.bases.mutable.IntegerAbstract;
import com.bergerkiller.bukkit.common.bases.mutable.ObjectAbstract;
import com.bergerkiller.bukkit.common.bases.mutable.VectorAbstract;
import com.bergerkiller.bukkit.common.conversion.Conversion;
import com.bergerkiller.bukkit.common.entity.CommonEntity;
import com.bergerkiller.bukkit.common.entity.CommonEntityController;
import com.bergerkiller.bukkit.common.entity.nms.NMSEntityTrackerEntry;
import com.bergerkiller.bukkit.common.protocol.CommonPacket;
import com.bergerkiller.bukkit.common.protocol.PacketType;
import com.bergerkiller.bukkit.common.reflection.classes.EntityLivingRef;
import com.bergerkiller.bukkit.common.reflection.classes.EntityRef;
import com.bergerkiller.bukkit.common.reflection.classes.EntityTrackerEntryRef;
import com.bergerkiller.bukkit.common.utils.CommonUtil;
import com.bergerkiller.bukkit.common.utils.EntityUtil;
import com.bergerkiller.bukkit.common.utils.LogicUtil;
import com.bergerkiller.bukkit.common.utils.MathUtil;
import com.bergerkiller.bukkit.common.utils.PacketUtil;
import com.bergerkiller.bukkit.common.utils.PlayerUtil;
import com.bergerkiller.bukkit.common.wrappers.DataWatcher;
/**
* A controller that deals with the server to client network synchronization.
*
* @param <T> - type of Common Entity this controller is for
*/
public abstract class EntityNetworkController<T extends CommonEntity<?>> extends CommonEntityController<T> {
/**
* The maximum allowed distance per relative movement update
*/
public static final int MAX_RELATIVE_DISTANCE = 127;
/**
* The minimum value change that is able to trigger an update
*/
public static final int MIN_RELATIVE_CHANGE = 4;
/**
* The minimum velocity change that is able to trigger an update
*/
public static final double MIN_RELATIVE_VELOCITY = 0.02;
/**
* The tick interval at which the entity is updated absolutely
*/
public static final int ABSOLUTE_UPDATE_INTERVAL = 400;
private Object handle;
/**
* Obtains the velocity as the clients know it, allowing it to be read from or written to
*/
public VectorAbstract velSynched = new VectorAbstract() {
public double getX() {return ((EntityTrackerEntry) handle).j;}
public double getY() {return ((EntityTrackerEntry) handle).k;}
public double getZ() {return ((EntityTrackerEntry) handle).l;}
public VectorAbstract setX(double x) {((EntityTrackerEntry) handle).j = x; return this;}
public VectorAbstract setY(double y) {((EntityTrackerEntry) handle).k = y; return this;}
public VectorAbstract setZ(double z) {((EntityTrackerEntry) handle).l = z; return this;}
};
/**
* Obtains the live protocol velocity, allowing it to be read from or written to
*/
public VectorAbstract velLive = new VectorAbstract() {
public double getX() {return entity.vel.getX();}
public double getY() {return entity.vel.getY();}
public double getZ() {return entity.vel.getZ();}
public VectorAbstract setX(double x) {entity.vel.setX(x); return this;}
public VectorAbstract setY(double y) {entity.vel.setY(y); return this;}
public VectorAbstract setZ(double z) {entity.vel.setZ(z); return this;}
};
/**
* Obtains the protocol location as the clients know it, allowing it to be read from or written to
*/
public IntLocationAbstract locSynched = new IntLocationAbstract() {
public World getWorld() {return entity.getWorld();}
public IntLocationAbstract setWorld(World world) {entity.setWorld(world); return this;}
public int getX() {return ((EntityTrackerEntry) handle).xLoc;}
public int getY() {return ((EntityTrackerEntry) handle).yLoc;}
public int getZ() {return ((EntityTrackerEntry) handle).zLoc;}
public IntLocationAbstract setX(int x) {((EntityTrackerEntry) handle).xLoc = x; return this;}
public IntLocationAbstract setY(int y) {((EntityTrackerEntry) handle).yLoc = y; return this;}
public IntLocationAbstract setZ(int z) {((EntityTrackerEntry) handle).zLoc = z; return this;}
public int getYaw() {return ((EntityTrackerEntry) handle).yRot;}
public int getPitch() {return ((EntityTrackerEntry) handle).xRot;}
public IntLocationAbstract setYaw(int yaw) {((EntityTrackerEntry) handle).yRot = yaw; return this;}
public IntLocationAbstract setPitch(int pitch) {((EntityTrackerEntry) handle).xRot = pitch; return this;}
};
/**
* Obtains the protocol location as it is live, on the server. Read is mainly supported, writing to it is not recommended.
* Although it has valid setters, the loss of accuracy of the protocol values make it rather pointless to use.
*/
public IntLocationAbstract locLive = new IntLocationAbstract() {
public World getWorld() {return entity.getWorld();}
public IntLocationAbstract setWorld(World world) {entity.setWorld(world); return this;}
public int getX() {return protLoc(entity.loc.getX());}
public int getY() {return MathUtil.floor(entity.loc.getY() * 32.0);}
public int getZ() {return protLoc(entity.loc.getZ());}
public IntLocationAbstract setX(int x) {entity.loc.setX(x >> 5); return this;}
public IntLocationAbstract setY(int y) {entity.loc.setY(y >> 5); return this;}
public IntLocationAbstract setZ(int z) {entity.loc.setZ(z >> 5); return this;}
public int getYaw() {return protRot(entity.loc.getYaw());}
public int getPitch() {return protRot(entity.loc.getPitch());}
public IntLocationAbstract setYaw(int yaw) {entity.loc.setYaw((float) yaw / 256.0f * 360.0f); return this;}
public IntLocationAbstract setPitch(int pitch) {entity.loc.setPitch((float) pitch / 256.0f * 360.0f); return this;}
};
/**
* Obtains the protocol head rotation as the clients know it, allowing it to be read from or written to
*/
public IntegerAbstract headRotSynched = new IntegerAbstract() {
public int get() {return ((EntityTrackerEntry) handle).i;}
public IntegerAbstract set(int value) {((EntityTrackerEntry) handle).i = value; return this;}
};
/**
* Obtains the protocol head rotation as it is live, on the server. Only reading is supported.
*/
public IntegerAbstract headRotLive = new IntegerAbstract() {
public int get() {return protRot(entity.getHeadRotation());}
public IntegerAbstract set(int value) {throw new UnsupportedOperationException();}
};
/**
* Obtains the tick time, this is for how long this network component/entry has been running on the server.
* The tick time can be used to perform operations on an interval.
* The tick time is automatically updated behind the hood.
*/
public IntegerAbstract ticks = new IntegerAbstract() {
public int get() {return ((EntityTrackerEntry) handle).m;}
public IntegerAbstract set(int value) {((EntityTrackerEntry) handle).m = value; return this;}
};
/**
* Obtains the vehicle of this (passenger) Entity as the clients know it, allowing it to be read from or written to
*/
public ObjectAbstract<Entity> vehicleSynched = new ObjectAbstract<Entity>() {
public Entity get() {return EntityTrackerEntryRef.vehicle.get(handle);}
public ObjectAbstract<Entity> set(Entity value) {EntityTrackerEntryRef.vehicle.set(handle, value); return this;}
};
public int getViewDistance() {
return EntityTrackerEntryRef.viewDistance.get(handle);
}
public void setViewDistance(int blockDistance) {
EntityTrackerEntryRef.viewDistance.set(handle, blockDistance);
}
public int getUpdateInterval() {
return EntityTrackerEntryRef.updateInterval.get(handle);
}
public void setUpdateInterval(int tickInterval) {
EntityTrackerEntryRef.updateInterval.set(handle, tickInterval);
}
public boolean isMobile() {
return EntityTrackerEntryRef.isMobile.get(handle);
}
public void setMobile(boolean mobile) {
EntityTrackerEntryRef.isMobile.set(handle, mobile);
}
/**
* Gets the amount of ticks that have passed since the last Location synchronization.
* A location synchronization means that an absolute position update is performed.
*
* @return ticks since last location synchronization
*/
public int getTicksSinceLocationSync() {
return EntityTrackerEntryRef.timeSinceLocationSync.get(handle);
}
/**
* Checks whether the current update interval is reached
*
* @return True if the update interval was reached, False if not
*/
public boolean isUpdateTick() {
return ticks.isMod(getUpdateInterval());
}
/**
* Binds this Entity Network Controller to an Entity.
* This is called from elsewhere, and should be ignored entirely.
*
* @param entity to bind with
* @param entityTrackerEntry to bind with
*/
public final void bind(T entity, Object entityTrackerEntry) {
if (this.entity != null) {
this.onDetached();
}
this.entity = entity;
this.handle = entityTrackerEntry;
if (this.handle instanceof NMSEntityTrackerEntry) {
((NMSEntityTrackerEntry) this.handle).setController(this);
}
if (this.entity.isSpawned()) {
this.onAttached();
}
}
/**
* Obtains the Entity Tracker Entry handle of this Network Controller
*
* @return entry handle
*/
public Object getHandle() {
return handle;
}
/**
* Gets a collection of all Players viewing this Entity
*
* @return viewing players
*/
public final Collection<Player> getViewers() {
return Collections.unmodifiableCollection(EntityTrackerEntryRef.viewers.get(handle));
}
/**
* Adds a new viewer to this Network Controller.
* Calling this method also results in spawn messages being sent to the viewer.
* When overriding, make sure to always check the super-result before continuing!
*
* @param viewer to add
* @return True if the viewer was added, False if the viewer was already added
*/
@SuppressWarnings("unchecked")
public boolean addViewer(Player viewer) {
if (!((EntityTrackerEntry) handle).trackedPlayers.add(Conversion.toEntityHandle.convert(viewer))) {
return false;
}
this.makeVisible(viewer);
return true;
}
/**
* Removes a viewer from this Network Controller.
* Calling this method also results in destroy messages being sent to the viewer.
* When overriding, make sure to always check the super-result before continuing!
*
* @param viewer to remove
* @return True if the viewer was removed, False if the viewer wasn't contained
*/
public boolean removeViewer(Player viewer) {
if (!((EntityTrackerEntry) handle).trackedPlayers.remove(Conversion.toEntityHandle.convert(viewer))) {
return false;
}
this.makeHidden(viewer);
return true;
}
/**
* Adds or removes a viewer based on viewer distance
*
* @param viewer to update
*/
public final void updateViewer(Player viewer) {
if (isViewable(viewer)) {
addViewer(viewer);
return;
}
// Check that the passenger of this Entity is not still viewable
// We can not hide vehicle of passengers that are still viewable...
if (entity.hasPassenger()) {
EntityNetworkController<?> network = CommonEntity.get(entity.getPassenger()).getNetworkController();
if (network != null && network.getViewers().contains(viewer)) {
addViewer(viewer);
return;
}
}
// No longer a viewer
removeViewer(viewer);
}
private boolean isViewable(Player viewer) {
// View range check
final int dx = MathHelper.floor(Math.abs(EntityUtil.getLocX(viewer) - (double) (this.locSynched.getX() / 32.0)));
final int dz = MathHelper.floor(Math.abs(EntityUtil.getLocZ(viewer) - (double) (this.locSynched.getZ() / 32.0)));
final int view = this.getViewDistance();
if (dx > view || dz > view) {
return false;
}
// The entity is in a chunk not seen by the viewer
if (!EntityRef.ignoreChunkCheck.get(entity.getHandle()) &&
!PlayerUtil.isChunkEntered(viewer, entity.getChunkX(), entity.getChunkZ())) {
return false;
}
// Entity is a Player hidden from sight for the viewer?
if (entity.getEntity() instanceof Player && !viewer.canSee((Player) entity.getEntity())) {
return false;
}
// It can be seen
return true;
}
/**
* Sets whether this Entity is marked for removal during the next tick for the player
*
* @param player to set it for
* @param remove - True to remove, False NOT to remove
*/
public void setRemoveNextTick(Player player, boolean remove) {
LogicUtil.addOrRemove(Common.SERVER.getEntityRemoveQueue(player), entity.getEntityId(), remove);
}
/**
* Ensures that the Entity is no longer displayed to the viewer
*
* @param viewer to hide this Entity for
* @param instant option: True to instantly hide, False to queue it for the next tick
*/
public void makeHidden(Player viewer, boolean instant) {
// If instant, do not send other destroy messages, if not, send one
this.setRemoveNextTick(viewer, !instant);
if (instant) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_DESTROY.newInstance(entity.getEntityId()));
}
}
/**
* Ensures that the Entity is no longer displayed to the viewer.
* The entity is not instantly hidden; it is queued for the next tick.
*
* @param viewer to hide this Entity for
*/
public void makeHidden(Player viewer) {
makeHidden(viewer, false);
}
/**
* Ensures that the Entity is no longer displayed to any viewers.
* All viewers will see the Entity disappear.
*
* @param instant option: True to instantly hide, False to queue it for the next tick
*/
public void makeHiddenForAll(boolean instant) {
for (Player viewer : getViewers()) {
makeHidden(viewer, instant);
}
}
/**
* Ensures that the Entity is no longer displayed to any viewers.
* All viewers will see the Entity disappear. This method queues for the next tick.
*/
public void makeHiddenForAll() {
for (Player viewer : getViewers()) {
makeHidden(viewer);
}
}
/**
* Ensures that the Entity is displayed to the viewer
*
* @param viewer to display this Entity for
*/
public void makeVisible(Player viewer) {
// We just made it visible - do not try to remove it
setRemoveNextTick(viewer, false);
// Spawn packet
PacketUtil.sendPacket(viewer, getSpawnPacket());
// Meta Data
initMetaData(viewer);
// Velocity
if (this.isMobile()) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_VELOCITY.newInstance(entity.getEntityId(), this.velSynched.vector()));
}
// Passenger/Vehicle information
if (entity.isInsideVehicle()) {
PacketUtil.sendPacket(viewer, getVehiclePacket(entity.getVehicle()));
}
if (entity.hasPassenger()) {
PacketUtil.sendPacket(viewer, getPassengerPacket(entity.getPassenger()));
}
// Potential leash
Entity leashHolder = entity.getLeashHolder();
if (leashHolder != null) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_ATTACH.newInstance(entity.getEntity(), leashHolder, 1));
}
// Human entity sleeping action
if (entity.getEntity() instanceof HumanEntity && ((HumanEntity) entity.getEntity()).isSleeping()) {
PacketUtil.sendPacket(viewer, PacketType.OUT_BED.newInstance((HumanEntity) entity.getEntity(),
entity.loc.x.block(), entity.loc.y.block(), entity.loc.z.block()));
}
// Initial entity head rotation
int headRot = headRotLive.get();
if (headRot != 0) {
PacketUtil.sendPacket(viewer, getHeadRotationPacket(headRot));
}
}
/**
* Synchronizes all Entity Meta Data including Entity Attributes and other specific flags.
* Movement and positioning information is not updated.<br><br>
*
* This should be called when making this Entity visible to a viewer.
*
* @param viewer to send the meta data to
*/
@SuppressWarnings("unchecked")
public void initMetaData(Player viewer) {
// Meta Data
DataWatcher metaData = entity.getMetaData();
if (!metaData.isEmpty()) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_METADATA.newInstance(entity.getEntityId(), metaData, true));
}
// Living Entity - only data
if (handle instanceof EntityLiving) {
// Entity Attributes
AttributeMapServer attributeMap = (AttributeMapServer) EntityLivingRef.getAttributesMap.invoke(handle);
Collection<?> attributes = attributeMap.c();
if (!attributes.isEmpty()) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_UPDATE_ATTRIBUTES.newInstance(entity.getEntityId(), attributes));
}
// Entity Equipment
EntityLiving living = (EntityLiving) handle;
for (int i = 0; i < 5; ++i) {
org.bukkit.inventory.ItemStack itemstack = Conversion.toItemStack.convert(living.getEquipment(i));
if (itemstack != null) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_EQUIPMENT.newInstance(entity.getEntityId(), i, itemstack));
}
}
// Entity Mob Effects
for (MobEffect effect : (Collection<MobEffect>) living.getEffects()) {
PacketUtil.sendPacket(viewer, PacketType.OUT_ENTITY_EFFECT_ADD.newInstance(entity.getEntityId(), effect));
}
}
}
/**
* Synchronizes everything by first destroying and then respawning this Entity to all viewers
*/
public void syncRespawn() {
// Hide
for (Player viewer : getViewers()) {
this.makeHidden(viewer, true);
}
// Update information
velSynched.set(velLive);
locSynched.set(locLive);
headRotSynched.set(headRotLive.get());
// Spawn
for (Player viewer : getViewers()) {
this.makeVisible(viewer);
}
}
/**
* Called at a set interval to synchronize data to clients
*/
public void onSync() {
if (entity.isDead()) {
return;
}
//TODO: Item frame support? Meh. Not for now.
// Vehicle
this.syncVehicle();
// Position / Rotation
if (this.isUpdateTick() || entity.isPositionChanged()) {
entity.setPositionChanged(false);
// Update location
if (this.getTicksSinceLocationSync() > ABSOLUTE_UPDATE_INTERVAL) {
this.syncLocationAbsolute();
} else {
this.syncLocation();
}
// Update velocity when position changes
this.syncVelocity();
}
// Refresh/resend velocity when requested (isVelocityChanged sets this)
if (entity.isVelocityChanged()) {
entity.setVelocityChanged(false);
Vector velocity = velLive.vector();
boolean cancelled = false;
if (entity.getEntity() instanceof Player) {
PlayerVelocityEvent event = new PlayerVelocityEvent((Player) entity.getEntity(), velocity);
if (CommonUtil.callEvent(event).isCancelled()) {
cancelled = true;
} else if (!velocity.equals(event.getVelocity())) {
velocity = event.getVelocity();
velLive.set(velocity);
}
}
// Send update packet if not cancelled
if (!cancelled) {
this.broadcast(getVelocityPacket(velocity.getX(), velocity.getY(), velocity.getZ()));
}
}
// Meta Data
this.syncMetaData();
// Head rotation
this.syncHeadRotation();
}
/**
* Checks whether one of the position (protocol) component differences
* between live and synched exceed the minimum change provided.
* In short, it checks whether the position changed.
*
* @param minChange to look for
* @return True if changed, False if not
*/
public boolean isPositionChanged(int minChange) {
return Math.abs(locLive.getX() - locSynched.getX()) >= minChange
|| Math.abs(locLive.getY() - locSynched.getY()) >= minChange
|| Math.abs(locLive.getZ() - locSynched.getZ()) >= minChange;
}
/**
* Checks whether one of the rotation (protocol) component differences
* between live and synched exceed the minimum change provided.
* In short, it checks whether the rotation changed.
*
* @param minChange to look for
* @return True if changed, False if not
*/
public boolean isRotationChanged(int minChange) {
return Math.abs(locLive.getYaw() - locSynched.getYaw()) >= minChange
|| Math.abs(locLive.getPitch() - locSynched.getPitch()) >= minChange;
}
/**
* Checks whether the velocity difference
* between live and synched exceeds the minimum change provided.
* In short, it checks whether the velocity changed.
*
* @param minChange to look for
* @return True if changed, False if not
*/
public boolean isVelocityChanged(double minChange) {
return velLive.distanceSquared(velSynched) > (minChange * minChange);
}
/**
* Checks whether the head rotation difference
* between live and synched exceeds the minimum change provided.
* In short, it checks whether the head rotation changed.
*
* @param minChange to look for
* @return True if changed, False if not
*/
public boolean isHeadRotationChanged(int minChange) {
return Math.abs(headRotLive.get() - headRotSynched.get()) >= minChange;
}
/**
* Synchronizes all Entity Meta Data including Entity Attributes and other specific flags.
* Movement and positioning information is not updated.
* Only the changes are sent, it is a relative update.
*/
public void syncMetaData() {
// Meta Data
DataWatcher meta = entity.getMetaData();
if (meta.isChanged()) {
broadcast(PacketType.OUT_ENTITY_METADATA.newInstance(entity.getEntityId(), meta, false), true);
}
// Living Entity - only data
if (handle instanceof EntityLiving) {
// Entity Attributes
AttributeMapServer attributeMap = (AttributeMapServer) EntityLivingRef.getAttributesMap.invoke(handle);
Collection<?> attributes = attributeMap.c();
if (!attributes.isEmpty()) {
this.broadcast(PacketType.OUT_ENTITY_UPDATE_ATTRIBUTES.newInstance(entity.getEntityId(), attributes), true);
}
attributes.clear();
}
}
/**
* Synchronizes the entity Vehicle to all viewers.
* Updates when the vehicle changes.
*/
public void syncVehicle() {
syncVehicle(entity.getVehicle());
}
/**
* Synchronizes the entity Vehicle
*
* @param vehicle to synchronize, NULL for no Vehicle
*/
public void syncVehicle(org.bukkit.entity.Entity vehicle) {
if (vehicleSynched.get() != vehicle) {
vehicleSynched.set(vehicle);
broadcast(getVehiclePacket(vehicle));
}
}
/**
* Synchronizes the entity head yaw rotation to all viewers.
*/
public void syncHeadRotation() {
if (isHeadRotationChanged(MIN_RELATIVE_CHANGE)) {
syncHeadRotation(headRotLive.get());
}
}
/**
* Synchronizes the entity head yaw rotation to all viewers.
*
* @param headRotation to set to
*/
public void syncHeadRotation(int headRotation) {
headRotSynched.set(headRotation);
this.broadcast(getHeadRotationPacket(headRotation));
}
/**
* Synchronizes the entity velocity to all viewers.
* Based on a change in Velocity, velocity will be updated.
*/
public void syncVelocity() {
if (!this.isMobile()) {
return;
}
if ((velLive.lengthSquared() == 0.0 && velSynched.lengthSquared() > 0.0) || isVelocityChanged(MIN_RELATIVE_VELOCITY)) {
this.syncVelocity(velLive.getX(), velLive.getY(), velLive.getZ());
}
}
/**
* Synchronizes the entity velocity
*
* @param velocity (new)
*/
public void syncVelocity(Vector velocity) {
syncVelocity(velocity.getX(), velocity.getY(), velocity.getZ());
}
/**
* Synchronizes the entity velocity
*
* @param velX
* @param velY
* @param velZ
*/
public void syncVelocity(double velX, double velY, double velZ) {
velSynched.set(velX, velY, velZ);
// If inside a vehicle, there is no use in updating
if (entity.isInsideVehicle()) {
return;
}
this.broadcast(getVelocityPacket(velX, velY, velZ));
}
/**
* Synchronizes the entity location to all clients.
* Based on the distances, relative or absolute movement is performed.
*/
public void syncLocation() {
syncLocation(isPositionChanged(MIN_RELATIVE_CHANGE), isRotationChanged(MIN_RELATIVE_CHANGE));
}
/**
* Synchronizes the entity position / rotation absolutely
*/
public void syncLocationAbsolute() {
syncLocationAbsolute(locLive.getX(), locLive.getY(), locLive.getZ(), locLive.getYaw(), locLive.getPitch());
}
/**
* Synchronizes the entity position / rotation absolutely
*
* @param posX - protocol position X
* @param posY - protocol position Y
* @param posZ - protocol position Z
* @param yaw - protocol rotation yaw
* @param pitch - protocol rotation pitch
*/
public void syncLocationAbsolute(int posX, int posY, int posZ, int yaw, int pitch) {
// Update protocol values
locSynched.set(posX, posY, posZ, yaw, pitch);
// Update last synchronization time
EntityTrackerEntryRef.timeSinceLocationSync.set(handle, 0);
// Send synchronization messages
broadcast(getLocationPacket(posX, posY, posZ, (byte) yaw, (byte) pitch));
}
/**
* Synchronizes the entity position / rotation relatively.
*
* @param position - whether to sync position
* @param rotation - whether to sync rotation
*/
public void syncLocation(boolean position, boolean rotation) {
if (!position && !rotation) {
return;
}
syncLocation(position, rotation, locLive.getX(), locLive.getY(), locLive.getZ(), locLive.getYaw(), locLive.getPitch());
}
/**
* Synchronizes the entity position / rotation relatively.
* If the relative change is too big, an absolute update is performed instead.
*
* @param position - whether to update position (read pos on/off)
* @param rotation - whether to update rotation (read yawpitch on/off)
* @param posX - protocol position X
* @param posY - protocol position Y
* @param posZ - protocol position Z
* @param yaw - protocol rotation yaw
* @param pitch - protocol rotation pitch
*/
public void syncLocation(boolean position, boolean rotation, int posX, int posY, int posZ, int yaw, int pitch) {
// No position updates allowed for passengers (this is FORCED). Rotation is allowed.
if (position && !entity.isInsideVehicle()) {
final int deltaX = posX - locSynched.getX();
final int deltaY = posY - locSynched.getY();
final int deltaZ = posZ - locSynched.getZ();
// There is no use sending relative updates with zero change...
if (deltaX == 0 && deltaY == 0 && deltaZ == 0) {
return;
}
// Absolute updates for too long distances
if (Math.abs(deltaX) > MAX_RELATIVE_DISTANCE || Math.abs(deltaY) > MAX_RELATIVE_DISTANCE || Math.abs(deltaZ) > MAX_RELATIVE_DISTANCE) {
// Distance too large, perform absolute update
// If no rotation is being updated, set the rotation to the synched rotation
if (!rotation) {
yaw = locSynched.getYaw();
pitch = locSynched.getPitch();
}
syncLocationAbsolute(posX, posY, posZ, yaw, pitch);
} else if (rotation) {
// Update rotation and position relatively
locSynched.set(posX, posY, posZ, yaw, pitch);
broadcast(PacketType.OUT_ENTITY_MOVE_LOOK.newInstance(entity.getEntityId(),
(byte) deltaX, (byte) deltaY, (byte) deltaZ, (byte) yaw, (byte) pitch));
} else {
// Only update position relatively
locSynched.set(posX, posY, posZ);
broadcast(PacketType.OUT_ENTITY_MOVE.newInstance(entity.getEntityId(),
(byte) deltaX, (byte) deltaY, (byte) deltaZ));
}
} else if (rotation) {
// Only update rotation
locSynched.setRotation(yaw, pitch);
broadcast(PacketType.OUT_ENTITY_LOOK.newInstance(entity.getEntityId(), (byte) yaw, (byte) pitch));
}
}
/*
* ================================================
* ===== Very basic protocol-related methods =====
* ================================================
*/
/**
* Sends a packet to all viewers, excluding the entity itself
*
* @param packet to send
*/
public void broadcast(CommonPacket packet) {
broadcast(packet, false);
}
/**
* Sends a packet to all viewers, and if set, to itself
*
* @param packet to send
* @param self option: True to send to self (if a player), False to not send to self
*/
public void broadcast(CommonPacket packet, boolean self) {
if (self && entity.getEntity() instanceof Player) {
PacketUtil.sendPacket((Player) entity.getEntity(), packet);
}
// Viewers
for (Player viewer : this.getViewers()) {
PacketUtil.sendPacket(viewer, packet);
}
}
/**
* Gets a new packet with absolute Entity position information
*
* @param posX - position X (protocol)
* @param posY - position Y (protocol)
* @param posZ - position Z (protocol)
* @param yaw - position yaw (protocol)
* @param pitch - position pitch (protocol)
* @return a packet with absolute position information
*/
public CommonPacket getLocationPacket(int posX, int posY, int posZ, int yaw, int pitch) {
return PacketType.OUT_ENTITY_TELEPORT.newInstance(entity.getEntityId(), posX, posY, posZ, (byte) yaw, (byte) pitch);
}
/**
* Gets a new packet with velocity information for this Entity
*
* @param velX - velocity X
* @param velY - velocity Y
* @param velZ - velocity Z
* @return a packet with velocity information
*/
public CommonPacket getVelocityPacket(double velX, double velY, double velZ) {
return PacketType.OUT_ENTITY_VELOCITY.newInstance(entity.getEntityId(), velX, velY, velZ);
}
/**
* Creates a new spawn packet for spawning this Entity.
* To change the spawned entity type, override this method.
* By default, the entity is evaluated and the right packet is created automatically.
*
* @return spawn packet
*/
public CommonPacket getSpawnPacket() {
final CommonPacket packet = EntityTrackerEntryRef.getSpawnPacket(handle);
if (PacketType.OUT_ENTITY_SPAWN.isInstance(packet)) {
// NMS error: They are not using the position, but the live position
// This has some big issues when new players join...
// Position
packet.write(PacketType.OUT_ENTITY_SPAWN.x, locSynched.getX());
packet.write(PacketType.OUT_ENTITY_SPAWN.y, locSynched.getY());
packet.write(PacketType.OUT_ENTITY_SPAWN.z, locSynched.getZ());
// Rotation
packet.write(PacketType.OUT_ENTITY_SPAWN.yaw, (byte) locSynched.getYaw());
packet.write(PacketType.OUT_ENTITY_SPAWN.pitch, (byte) locSynched.getPitch());
}
return packet;
}
/**
* Gets a new packet with information for this Entity
*
* @param passenger that is now inside this vehicle Entity
* @return packet with passenger information
*/
public CommonPacket getPassengerPacket(Entity passenger) {
return PacketType.OUT_ENTITY_ATTACH.newInstance(passenger, entity.getEntity());
}
/**
* Gets a new packet with vehicle information for this Entity
*
* @param vehicle this Entity is now a passenger of
* @return packet with vehicle information
*/
public CommonPacket getVehiclePacket(Entity vehicle) {
return PacketType.OUT_ENTITY_ATTACH.newInstance(entity.getEntity(), vehicle);
}
/**
* Gets a new packet with head rotation information for this Entity
*
* @param headRotation value (protocol value)
* @return packet with head rotation information
*/
public CommonPacket getHeadRotationPacket(int headRotation) {
return PacketType.OUT_ENTITY_HEAD_ROTATION.newInstance(entity.getEntity(), (byte) headRotation);
}
private int protRot(float rot) {
return MathUtil.floor(rot * 256.0f / 360.0f);
}
private int protLoc(double loc) {
return ((EntityTrackerEntry) handle).tracker.as.a(loc);
}
}