/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.network.entity.vanilla;
import static org.lanternpowered.server.network.vanilla.message.codec.play.CodecUtils.wrapAngle;
import com.flowpowered.math.vector.Vector3d;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import org.lanternpowered.server.data.key.LanternKeys;
import org.lanternpowered.server.entity.LanternEntity;
import org.lanternpowered.server.entity.LanternLiving;
import org.lanternpowered.server.entity.event.CollectEntityEvent;
import org.lanternpowered.server.entity.event.EntityEvent;
import org.lanternpowered.server.network.buffer.ByteBuffer;
import org.lanternpowered.server.network.buffer.ByteBufferAllocator;
import org.lanternpowered.server.network.entity.AbstractEntityProtocol;
import org.lanternpowered.server.network.entity.EntityProtocolUpdateContext;
import org.lanternpowered.server.network.entity.parameter.ByteBufParameterList;
import org.lanternpowered.server.network.entity.parameter.EmptyParameterList;
import org.lanternpowered.server.network.entity.parameter.ParameterList;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutDestroyEntities;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityCollectItem;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityHeadLook;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityLook;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityLookAndRelativeMove;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityMetadata;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityRelativeMove;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityTeleport;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityVelocity;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSetEntityPassengers;
import org.lanternpowered.server.text.LanternTexts;
import org.spongepowered.api.data.key.Keys;
import org.spongepowered.api.entity.Entity;
import org.spongepowered.api.entity.living.Living;
import java.util.Collections;
import java.util.List;
public abstract class EntityProtocol<E extends LanternEntity> extends AbstractEntityProtocol<E> {
private double lastX;
private double lastY;
private double lastZ;
private double lastYaw;
private double lastPitch;
private double lastHeadYaw;
private double lastVelX;
private double lastVelY;
private double lastVelZ;
private byte lastFlags;
private List<Entity> lastPassengers = Collections.emptyList();
public EntityProtocol(E entity) {
super(entity);
}
@Override
protected void destroy(EntityProtocolUpdateContext context) {
context.sendToAllExceptSelf(new MessagePlayOutDestroyEntities(getRootEntityId()));
}
@Override
protected void update(EntityProtocolUpdateContext context) {
final Vector3d rot = this.entity.getRotation();
final Vector3d headRot = this.entity instanceof Living ? ((Living) this.entity).getHeadRotation() : null;
final Vector3d pos = this.entity.getPosition();
final double x = pos.getX();
final double y = pos.getY();
final double z = pos.getZ();
final double yaw = rot.getY();
// All living entities have a head rotation and changing the pitch
// would only affect the head pitch.
final double pitch = (headRot != null ? headRot : rot).getX();
boolean dirtyPos = x != this.lastX || y != this.lastY || z != this.lastZ;
boolean dirtyRot = yaw != this.lastYaw || z != this.lastPitch;
// TODO: On ground state
final int entityId = this.getRootEntityId();
if (dirtyRot) {
this.lastYaw = yaw;
this.lastPitch = pitch;
}
if (dirtyPos) {
double dx = x - this.lastX;
double dy = y - this.lastY;
double dz = z - this.lastZ;
this.lastX = x;
this.lastY = y;
this.lastZ = z;
// Don't send movement messages if the entity
// is a passengers, otherwise glitches will
// rule the world.
if (!this.entity.getVehicle().isPresent()) {
if (Math.abs(dx) < 8 && Math.abs(dy) < 8 && Math.abs(dz) < 8) {
int dxu = (int) (dx * 4096);
int dyu = (int) (dy * 4096);
int dzu = (int) (dz * 4096);
if (dirtyRot) {
context.sendToAllExceptSelf(new MessagePlayOutEntityLookAndRelativeMove(entityId,
dxu, dyu, dzu, wrapAngle(yaw), wrapAngle(pitch), false));
// The rotation is already send
dirtyRot = false;
} else {
context.sendToAllExceptSelf(new MessagePlayOutEntityRelativeMove(entityId,
dxu, dyu, dzu, false));
}
} else {
context.sendToAllExceptSelf(new MessagePlayOutEntityTeleport(entityId,
x, y, z, wrapAngle(yaw), wrapAngle(pitch), false));
// The rotation is already send
dirtyRot = false;
}
}
}
if (dirtyRot) {
context.sendToAllExceptSelf(() -> new MessagePlayOutEntityLook(entityId, wrapAngle(yaw), wrapAngle(pitch), false));
}
if (headRot != null) {
double headYaw = headRot.getY();
if (headYaw != this.lastHeadYaw) {
context.sendToAllExceptSelf(() -> new MessagePlayOutEntityHeadLook(entityId, wrapAngle(headYaw)));
}
this.lastHeadYaw = yaw;
}
final Vector3d velocity = this.entity.getVelocity();
final double vx = velocity.getX();
final double vy = velocity.getY();
final double vz = velocity.getZ();
if (vx != this.lastVelX || vy != this.lastVelY || vz != this.lastVelZ) {
context.sendToAll(() -> new MessagePlayOutEntityVelocity(entityId, vx, vy, vz));
this.lastVelX = vx;
this.lastVelY = vy;
this.lastVelZ = vz;
}
final ParameterList parameterList = context == EntityProtocolUpdateContext.empty() ?
fillParameters(false, EmptyParameterList.INSTANCE) : fillParameters(false);
// There were parameters applied
if (!parameterList.isEmpty()) {
context.sendToAll(() -> new MessagePlayOutEntityMetadata(entityId, parameterList));
}
// TODO: Update attributes
}
@Override
protected void handleEvent(EntityProtocolUpdateContext context, EntityEvent event) {
if (event instanceof CollectEntityEvent) {
final LanternLiving collector = (LanternLiving) ((CollectEntityEvent) event).getCollector();
context.getId(collector).ifPresent(id -> {
final int count = ((CollectEntityEvent) event).getCollectedItemsCount();
context.sendToAll(() -> new MessagePlayOutEntityCollectItem(id, getRootEntityId(), count));
});
} else {
super.handleEvent(context, event);
}
}
@Override
protected void postUpdate(EntityProtocolUpdateContext context) {
final List<Entity> passengers = this.entity.getPassengers();
if (!passengers.equals(this.lastPassengers)) {
this.lastPassengers = passengers;
sendPassengers(context, passengers);
}
}
@Override
public void postSpawn(EntityProtocolUpdateContext context) {
sendPassengers(context, this.entity.getPassengers());
}
private void sendPassengers(EntityProtocolUpdateContext context, List<Entity> passengers) {
context.sendToAll(() -> {
final IntList passengerIds = new IntArrayList();
for (Entity passenger : passengers) {
context.getId(passenger).ifPresent(passengerIds::add);
}
return new MessagePlayOutSetEntityPassengers(getRootEntityId(), passengerIds.toIntArray());
});
}
/**
* Fills a {@link ByteBuffer} with parameters to spawn or update the {@link Entity}.
*
* @param initial Whether the entity is being spawned, the byte buffer can be null if this is false
* @return The byte buffer
*/
ParameterList fillParameters(boolean initial) {
return fillParameters(initial, new ByteBufParameterList(ByteBufferAllocator.unpooled()));
}
private ParameterList fillParameters(boolean initial, ParameterList parameterList) {
if (initial) {
spawn(parameterList);
} else {
update(parameterList);
}
return parameterList;
}
protected boolean isSneaking() {
return false;
}
protected boolean isUsingItem() {
return false;
}
protected boolean isSprinting() {
return false;
}
/**
* Fills the {@link ParameterList} with parameters to spawn the {@link Entity} on
* the client.
*
* @param parameterList The parameter list to fill
*/
protected void spawn(ParameterList parameterList) {
parameterList.add(EntityParameters.Base.FLAGS, packFlags());
parameterList.add(EntityParameters.Base.AIR_LEVEL, getInitialAirLevel());
parameterList.add(EntityParameters.Base.CUSTOM_NAME, this.entity.get(Keys.DISPLAY_NAME).map(LanternTexts::toLegacy).orElse(""));
parameterList.add(EntityParameters.Base.CUSTOM_NAME_VISIBLE, this.entity.get(Keys.CUSTOM_NAME_VISIBLE).orElse(true));
parameterList.add(EntityParameters.Base.IS_SILENT, this.entity.get(Keys.IS_SILENT).orElse(false));
parameterList.add(EntityParameters.Base.NO_GRAVITY, hasNoGravity());
}
boolean hasNoGravity() {
// Always disable gravity for regular entities, we will handle our own physics
return true;
}
private byte packFlags() {
byte flags = 0;
if (this.entity.get(Keys.FIRE_TICKS).orElse(0) > 0) {
flags |= 0x01;
}
if (isSneaking()) {
flags |= 0x02;
}
if (isSprinting()) {
flags |= 0x08;
}
if (isUsingItem()) {
flags |= 0x10;
}
if (this.entity.get(Keys.INVISIBLE).orElse(false)) {
flags |= 0x20;
}
if (this.entity.get(Keys.GLOWING).orElse(false)) {
flags |= 0x40;
}
if (this.entity.get(LanternKeys.IS_ELYTRA_FLYING).orElse(false)) {
flags |= 0x80;
}
return flags;
}
/**
* Fills the {@link ParameterList} with parameters to update the {@link Entity} on
* the client.
*
* @param parameterList The parameter list to fill
*/
protected void update(ParameterList parameterList) {
final byte flags = packFlags();
if (flags != this.lastFlags) {
parameterList.add(EntityParameters.Base.FLAGS, flags);
this.lastFlags = flags;
}
}
/**
* Gets the air level of the entity.
*
* The air is by default 100 because the entities don't even use the air level,
* except for the players. This method can be overridden if needed.
*
* @return The air level
*/
protected short getInitialAirLevel() {
return 100;
}
}