/*
* 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.checkNotNull;
import com.flowpowered.math.vector.Vector3d;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.lanternpowered.server.entity.LanternEntity;
import org.lanternpowered.server.entity.event.EntityEvent;
import org.lanternpowered.server.entity.living.player.LanternPlayer;
import org.spongepowered.api.entity.Entity;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Consumer;
import javax.annotation.Nullable;
public final class EntityProtocolManager {
public static final int INVALID_ENTITY_ID = -1;
private static final int UPDATE_RATE = 3;
public static int acquireEntityId() {
return new EntityProtocolInitContextImpl(null).acquire();
}
public static void releaseEntityId(int id) {
new EntityProtocolInitContextImpl(null).release(id);
}
private final Map<Entity, AbstractEntityProtocol<?>> entityProtocols = new ConcurrentHashMap<>();
/**
* All the {@link AbstractEntityProtocol}s that will be destroyed.
*/
private final Queue<AbstractEntityProtocol<?>> queuedForRemoval = new ConcurrentLinkedDeque<>();
private final Int2ObjectMap<AbstractEntityProtocol<?>> idToEntityProtocolMap = new Int2ObjectOpenHashMap<>();
private static int allocatorIdCounter = 0;
private final static IntSet allocatorReusableIds = new IntOpenHashSet();
private final static IntIterator allocatorReusableIdsIterator = allocatorReusableIds.iterator();
private final static StampedLock allocatorLock = new StampedLock();
/**
* The {@link EntityProtocolInitContext}.
*/
private static class EntityProtocolInitContextImpl implements EntityProtocolInitContext {
@Nullable private final AbstractEntityProtocol<?> entityProtocol;
private EntityProtocolInitContextImpl(@Nullable AbstractEntityProtocol<?> entityProtocol) {
this.entityProtocol = entityProtocol;
}
/**
* Acquires the next free id.
*
* @return The id
*/
@Override
public int acquire() {
final long stamp = allocatorLock.writeLock();
try {
return acquire0();
} finally {
allocatorLock.unlockWrite(stamp);
}
}
@Override
public int[] acquire(int count) {
return acquire(new int[count]);
}
@Override
public int[] acquire(int[] array) {
checkNotNull(array, "array");
final long stamp = allocatorLock.writeLock();
try {
for (int i = 0; i < array.length; i++) {
array[i] = acquire0();
}
} finally {
allocatorLock.unlockWrite(stamp);
}
return array;
}
@Override
public int[] acquireRow(int count) {
return acquireRow(new int[count]);
}
@Override
public int[] acquireRow(int[] array) {
checkNotNull(array, "array");
final long stamp = allocatorLock.writeLock();
try {
final IntIterator iterator = allocatorReusableIds.iterator();
boolean fail = false;
for (int i = 0; i < array.length; i++) {
if (!iterator.hasNext()) {
fail = true;
break;
}
array[i] = iterator.next();
if (i != 0 && array[i - 1] != array[i] - 1) {
fail = true;
break;
}
}
if (fail) {
for (int i = 0; i < array.length; i++) {
array[i] = allocatorIdCounter++;
if (this.entityProtocol != null) {
this.entityProtocol.entityProtocolManager.idToEntityProtocolMap.put(array[i], this.entityProtocol);
}
}
} else {
for (int id : array) {
allocatorReusableIdsIterator.nextInt();
allocatorReusableIdsIterator.remove();
if (this.entityProtocol != null) {
this.entityProtocol.entityProtocolManager.idToEntityProtocolMap.put(id, this.entityProtocol);
}
}
}
} finally {
allocatorLock.unlockWrite(stamp);
}
return array;
}
private int acquire0() {
final int id;
if (allocatorReusableIdsIterator.hasNext()) {
try {
id = allocatorReusableIdsIterator.nextInt();
} finally {
allocatorReusableIdsIterator.remove();
}
} else {
id = allocatorIdCounter++;
}
if (this.entityProtocol != null) {
this.entityProtocol.entityProtocolManager.idToEntityProtocolMap.put(id, this.entityProtocol);
}
return id;
}
@Override
public void release(int id) {
if (id != INVALID_ENTITY_ID) {
final long stamp = allocatorLock.writeLock();
try {
allocatorReusableIds.add(id);
if (this.entityProtocol != null) {
this.entityProtocol.entityProtocolManager.idToEntityProtocolMap.remove(id);
}
} finally {
allocatorLock.unlockWrite(stamp);
}
}
}
@Override
public void release(int[] array) {
checkNotNull(array, "array");
final long stamp = allocatorLock.writeLock();
try {
for (int id : array) {
allocatorReusableIds.add(id);
if (this.entityProtocol != null) {
this.entityProtocol.entityProtocolManager.idToEntityProtocolMap.remove(id);
}
}
} finally {
allocatorLock.unlockWrite(stamp);
}
}
}
private int pulseCounter;
Optional<AbstractEntityProtocol<?>> getEntityProtocolById(int id) {
long stamp = allocatorLock.tryOptimisticRead();
AbstractEntityProtocol<?> entityProtocol = stamp != 0L ? this.idToEntityProtocolMap.get(id) : null;
if (stamp == 0L || !allocatorLock.validate(stamp)) {
stamp = allocatorLock.readLock();
try {
entityProtocol = this.idToEntityProtocolMap.get(id);
} finally {
allocatorLock.unlockRead(stamp);
}
}
return Optional.ofNullable(entityProtocol);
}
Optional<AbstractEntityProtocol<?>> getEntityProtocolByEntity(Entity entity) {
return Optional.ofNullable(this.entityProtocols.get(entity));
}
/**
* Adds the {@link Entity} to be tracked.
*
* @param entity The entity
*/
public void add(LanternEntity entity) {
//noinspection ConstantConditions,unchecked
add(entity, (EntityProtocolType) entity.getEntityProtocolType());
}
/**
* Adds the {@link Entity} to be tracked with a specific {@link EntityProtocolType}.
*
* <p>This method forces the entity protocol to be refreshed, even if the entity
* already a protocol.<p/>
*
* @param entity The entity
* @param protocolType The protocol type
*/
public <E extends LanternEntity> void add(E entity, EntityProtocolType<E> protocolType) {
checkNotNull(entity, "entity");
checkNotNull(protocolType, "protocolType");
final AbstractEntityProtocol<E> entityProtocol = protocolType.getSupplier().apply(entity);
entityProtocol.entityProtocolManager = this;
final AbstractEntityProtocol<?> removed = this.entityProtocols.put(entity, entityProtocol);
if (removed != null) {
this.queuedForRemoval.add(removed);
}
entityProtocol.init(new EntityProtocolInitContextImpl(entityProtocol));
if (entity instanceof NetworkIdHolder) {
final long stamp = allocatorLock.writeLock();
try {
this.idToEntityProtocolMap.put(((NetworkIdHolder) entity).getNetworkId(), entityProtocol);
} finally {
allocatorLock.unlockWrite(stamp);
}
}
}
/**
* Removes the {@link Entity} from being tracked.
*
* @param entity The entity
*/
public void remove(LanternEntity entity) {
checkNotNull(entity, "entity");
final AbstractEntityProtocol<?> removed = this.entityProtocols.remove(entity);
if (removed != null) {
this.queuedForRemoval.add(removed);
}
}
/**
* Updates the trackers of the entities. The players list contains all the players that
* are in the same world of the entities.
*
* @param players The players
*/
public void updateTrackers(Set<LanternPlayer> players) {
// TODO: Sync the updates in a different thread?
if (this.pulseCounter++ % UPDATE_RATE != 0) {
return;
}
AbstractEntityProtocol<?> removed;
while ((removed = this.queuedForRemoval.poll()) != null) {
removed.destroy(new EntityProtocolInitContextImpl(removed));
}
final List<AbstractEntityProtocol.TrackerUpdateContextData> updateContextDataList = new ArrayList<>();
final Set<AbstractEntityProtocol<?>> protocols = new HashSet<>(this.entityProtocols.values());
for (AbstractEntityProtocol<?> protocol : protocols) {
final AbstractEntityProtocol.TrackerUpdateContextData contextData = protocol.buildUpdateContextData(players);
if (contextData != null) {
//noinspection unchecked
protocol.updateTrackers(contextData);
updateContextDataList.add(contextData);
}
}
for (AbstractEntityProtocol.TrackerUpdateContextData contextData : updateContextDataList) {
contextData.entityProtocol.postUpdateTrackers(contextData);
}
}
private static final int INTERACT_DELAY = 50;
public void playerInteract(LanternPlayer player, int entityId, @Nullable Vector3d position) {
playerUseEntity(player, entityId, entityProtocol -> entityProtocol.playerInteract(player, entityId, position));
}
public void playerAttack(LanternPlayer player, int entityId) {
playerUseEntity(player, entityId, entityProtocol -> entityProtocol.playerAttack(player, entityId));
}
private void playerUseEntity(LanternPlayer player, int entityId,
Consumer<AbstractEntityProtocol<?>> entityProtocolConsumer) {
getEntityProtocolById(entityId).ifPresent(entityProtocol -> {
synchronized (entityProtocol.playerInteractTimes) {
final long time = entityProtocol.playerInteractTimes.getLong(player);
final long current = System.currentTimeMillis();
if (time == 0L || current - time > INTERACT_DELAY) {
entityProtocolConsumer.accept(entityProtocol);
entityProtocol.playerInteractTimes.put(player, current);
}
}
});
}
public void triggerEvent(LanternEntity entity, EntityEvent event) {
getEntityProtocolByEntity(entity).ifPresent(entityProtocol -> {
synchronized (entityProtocol.entityEvents) {
entityProtocol.entityEvents.add(event);
}
});
}
}