/*
* 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;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.lanternpowered.server.network.entity.EntityProtocolManager.INVALID_ENTITY_ID;
import com.flowpowered.math.vector.Vector3d;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import org.lanternpowered.server.entity.LanternEntity;
import org.lanternpowered.server.entity.event.EntityEvent;
import org.lanternpowered.server.entity.event.EntityEventType;
import org.lanternpowered.server.entity.living.player.LanternPlayer;
import org.lanternpowered.server.network.message.Message;
import org.spongepowered.api.entity.Entity;
import org.spongepowered.api.entity.living.player.Player;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Supplier;
import javax.annotation.Nullable;
public abstract class AbstractEntityProtocol<E extends LanternEntity> {
@SuppressWarnings("NullableProblems") EntityProtocolManager entityProtocolManager;
/**
* All the players tracking this entity.
*/
private final Set<LanternPlayer> trackers = new HashSet<>();
/**
* The entity that is being tracked.
*/
protected final E entity;
/**
* The entity id of the entity.
*/
private int entityId = INVALID_ENTITY_ID;
/**
* The amount of ticks between every update.
*/
private int tickRate = 4;
/**
* The tracking range of the entity.
*/
private double trackingRange = 64;
private int tickCounter = 0;
final Object2LongMap<Player> playerInteractTimes = new Object2LongOpenHashMap<>();
final List<EntityEvent> entityEvents = new ArrayList<>();
public AbstractEntityProtocol(E entity) {
this.entity = entity;
}
private final class SimpleEntityProtocolContext implements EntityProtocolUpdateContext {
@SuppressWarnings("NullableProblems")
private Set<LanternPlayer> trackers;
@Override
public Optional<LanternEntity> getById(int entityId) {
return entityProtocolManager.getEntityProtocolById(entityId).map(AbstractEntityProtocol::getEntity);
}
@Override
public OptionalInt getId(Entity entity) {
checkNotNull(entity, "entity");
final Optional<AbstractEntityProtocol<?>> entityProtocol = entityProtocolManager.getEntityProtocolByEntity(entity);
return entityProtocol.isPresent() ? OptionalInt.of(entityProtocol.get().entityId) : OptionalInt.empty();
}
@Override
public void sendToSelf(Message message) {
if (entity instanceof Player) {
((LanternPlayer) entity).getConnection().send(message);
}
}
@Override
public void sendToSelf(Supplier<Message> messageSupplier) {
if (entity instanceof Player) {
sendToSelf(messageSupplier.get());
}
}
@Override
public void sendToAll(Message message) {
this.trackers.forEach(tracker -> tracker.getConnection().send(message));
}
@Override
public void sendToAll(Supplier<Message> message) {
if (!this.trackers.isEmpty()) {
sendToAll(message.get());
}
}
@Override
public void sendToAllExceptSelf(Message message) {
this.trackers.forEach(tracker -> {
if (tracker != entity) {
tracker.getConnection().send(message);
}
});
}
@Override
public void sendToAllExceptSelf(Supplier<Message> messageSupplier) {
if (!this.trackers.isEmpty()) {
sendToAllExceptSelf(messageSupplier.get());
}
}
}
public E getEntity() {
return this.entity;
}
protected int getRootEntityId() {
return this.entityId;
}
/**
* Sets the tick rate of this entity protocol.
*
* @param tickRate The tick rate
*/
public void setTickRate(int tickRate) {
this.tickRate = tickRate;
}
/**
* Gets the tick rate of this entity protocol.
*
* @return The tick rate
*/
public int getTickRate() {
return this.tickRate;
}
/**
* Gets the tracking range of the entity.
*
* @return The tracking range
*/
public double getTrackingRange() {
return this.trackingRange;
}
/**
* Sets the tracking range of the entity.
*
* @param trackingRange The tracking range
*/
public void setTrackingRange(double trackingRange) {
this.trackingRange = trackingRange;
}
/**
* Destroys the entity. This removes all the trackers and sends a destroy
* message to the client.
*
* @param context The entity protocol context
*/
void destroy(EntityProtocolInitContext context) {
if (!this.trackers.isEmpty()) {
// Destroy the entity on all the clients
final SimpleEntityProtocolContext ctx = new SimpleEntityProtocolContext();
final TempEvents events = processEvents(true, true);
ctx.trackers = this.trackers;
if (events != null && events.deathOrAlive != null) {
events.deathOrAlive.forEach(event -> handleEvent(ctx, event));
}
destroy(ctx);
this.trackers.clear();
synchronized (this.playerInteractTimes) {
this.playerInteractTimes.clear();
}
}
remove(context);
}
protected void remove(EntityProtocolInitContext context) {
// Release the entity id of the entity
if (!(this.entity instanceof NetworkIdHolder)) {
context.release(this.entityId);
}
this.entityId = INVALID_ENTITY_ID;
}
/**
* Initializes this entity protocol. This acquires the ids
* that are required to spawn the entity.
*
* @param context The entity protocol context
*/
protected void init(EntityProtocolInitContext context) {
if (this.entity instanceof NetworkIdHolder) {
initRootId(((NetworkIdHolder) this.entity).getNetworkId());
} else {
// Allocate the next free id
initRootId(context.acquire());
}
}
/**
* Initializes the root entity id of this protocol.
*
* @param rootEntityId The root entity id
*/
protected final void initRootId(int rootEntityId) {
checkArgument(rootEntityId != INVALID_ENTITY_ID, "The root entity id cannot be invalid.");
checkState(this.entityId == INVALID_ENTITY_ID, "This entity protocol is already initialized.");
this.entityId = rootEntityId;
}
final class TrackerUpdateContextData {
final AbstractEntityProtocol<?> entityProtocol;
final SimpleEntityProtocolContext ctx = new SimpleEntityProtocolContext();
@Nullable Set<LanternPlayer> added;
@Nullable Set<LanternPlayer> removed;
@Nullable Set<LanternPlayer> update;
TrackerUpdateContextData(AbstractEntityProtocol<?> entityProtocol) {
this.entityProtocol = entityProtocol;
}
}
@Nullable
TrackerUpdateContextData buildUpdateContextData(Set<LanternPlayer> players) {
players = new HashSet<>(players);
final Set<LanternPlayer> removed = new HashSet<>();
final Set<LanternPlayer> added = new HashSet<>();
final Vector3d pos = this.entity.getPosition();
final Iterator<LanternPlayer> trackerIt = this.trackers.iterator();
while (trackerIt.hasNext()) {
final LanternPlayer tracker = trackerIt.next();
final boolean flag = players.remove(tracker);
if (tracker != this.entity &&
(!flag || !isVisible(pos, tracker))) {
trackerIt.remove();
removed.add(tracker);
}
}
for (LanternPlayer tracker : players) {
if (tracker == this.entity || isVisible(pos, tracker)) {
added.add(tracker);
}
}
boolean flag0 = this.tickCounter++ % this.tickRate == 0 && !this.trackers.isEmpty();
boolean flag1 = !added.isEmpty();
boolean flag2 = !removed.isEmpty();
if (!flag0 && !flag1 && !flag2) {
return null;
}
final TrackerUpdateContextData contextData = new TrackerUpdateContextData(this);
if (flag0 || flag1) {
contextData.update = new HashSet<>(this.trackers);
}
if (flag1) {
contextData.added = added;
this.trackers.addAll(added);
}
if (flag2) {
contextData.removed = removed;
}
return contextData;
}
void updateTrackers(TrackerUpdateContextData contextData) {
final SimpleEntityProtocolContext ctx = contextData.ctx;
final TempEvents events = processEvents(contextData.removed != null, true);
if (contextData.removed != null) {
ctx.trackers = contextData.removed;
if (events != null && events.deathOrAlive != null) {
events.deathOrAlive.forEach(event -> handleEvent(ctx, event));
}
destroy(ctx);
synchronized (this.playerInteractTimes) {
contextData.removed.forEach(this.playerInteractTimes::remove);
}
}
Set<LanternPlayer> trackers = null;
if (contextData.update != null) {
ctx.trackers = contextData.update;
update(ctx);
if (events != null) {
trackers = contextData.added == null ? contextData.update : new HashSet<>(contextData.update);
}
}
if (contextData.added != null) {
ctx.trackers = contextData.added;
spawn(ctx);
if (events != null) {
if (trackers == null) {
trackers = contextData.added;
} else {
trackers.addAll(contextData.added);
}
}
}
if (trackers != null) {
ctx.trackers = trackers;
events.alive.forEach(event -> handleEvent(ctx, event));
}
}
private final class TempEvents {
@Nullable private final List<EntityEvent> deathOrAlive;
private final List<EntityEvent> alive;
private TempEvents(@Nullable List<EntityEvent> deathOrAlive, List<EntityEvent> alive) {
this.deathOrAlive = deathOrAlive;
this.alive = alive;
}
}
@Nullable
private TempEvents processEvents(boolean death, boolean alive) {
if (!death && !alive) {
return null;
}
List<EntityEvent> aliveList = null;
synchronized (this.entityEvents) {
if (!this.entityEvents.isEmpty()) {
aliveList = new ArrayList<>(this.entityEvents);
this.entityEvents.clear();
}
}
if (aliveList == null) {
return null;
}
List<EntityEvent> deathOrAliveList = null;
if (death) {
for (EntityEvent event : aliveList) {
if (event.type() == EntityEventType.DEATH_OR_ALIVE) {
if (deathOrAliveList == null) {
deathOrAliveList = new ArrayList<>();
}
deathOrAliveList.add(event);
}
}
}
return new TempEvents(deathOrAliveList, aliveList);
}
void postUpdateTrackers(TrackerUpdateContextData contextData) {
final SimpleEntityProtocolContext ctx = contextData.ctx;
if (contextData.update != null) {
ctx.trackers = contextData.update;
postUpdate(ctx);
}
if (contextData.added != null) {
ctx.trackers = contextData.added;
postSpawn(ctx);
}
}
private boolean isVisible(Vector3d pos, LanternPlayer tracker) {
return pos.distanceSquared(tracker.getPosition()) < this.trackingRange * this.trackingRange && isVisible(tracker);
}
/**
* Gets whether the tracked entity is visible for the tracker.
*
* @param tracker The tracker
* @return Whether the tracker can see the entity
*/
protected boolean isVisible(LanternPlayer tracker) {
return tracker.canSee(this.entity);
}
/**
* Spawns the tracked entity.
*
* @param context The entity update context
*/
protected abstract void spawn(EntityProtocolUpdateContext context);
/**
* Destroys the tracked entity.
*
* @param context The entity update context
*/
protected abstract void destroy(EntityProtocolUpdateContext context);
/**
* Updates the tracked entity.
*
* @param context The entity update context
*/
protected abstract void update(EntityProtocolUpdateContext context);
protected void handleEvent(EntityProtocolUpdateContext context, EntityEvent event) {
}
/**
* Post spawns the tracked entity. This method will be called after
* all the entities that were pending for updates/spawns are processed.
*
* @param context The entity update context
*/
protected void postSpawn(EntityProtocolUpdateContext context) {
}
/**
* Post updates the tracked entity. This method will be called after
* all the entities that were pending for updates/spawns are processed.
*
* @param context The entity update context
*/
protected void postUpdate(EntityProtocolUpdateContext context) {
}
/**
* Is called when the specified {@link LanternPlayer} tries to interact
* with this entity, or at least one of the ids assigned to it.
*
* @param player The player that interacted with the entity
* @param entityId The entity the player interacted with
* @param position The position where the player interacted with the entity, if present
*/
protected void playerInteract(LanternPlayer player, int entityId, @Nullable Vector3d position) {
}
/**
* Is called when the specified {@link LanternPlayer} tries to attach
* this entity, or at least one of the ids assigned to it.
*
* @param player The player that attack the entity
* @param entityId The entity id the player attacked
*/
protected void playerAttack(LanternPlayer player, int entityId) {
}
}