package tc.oc.pgm.mutation.types; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gson.reflect.TypeToken; import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.player.PlayerSpawnEntityEvent; import org.bukkit.inventory.EntityEquipment; import tc.oc.commons.core.collection.WeakHashSet; import tc.oc.pgm.events.MatchPlayerDeathEvent; import tc.oc.pgm.events.PlayerChangePartyEvent; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchPlayer; import javax.annotation.Nullable; import java.time.Instant; import java.util.Comparator; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.WeakHashMap; import java.util.stream.Stream; import static tc.oc.commons.core.util.Optionals.cast; /** * A mutation module that tracks entity spawns. */ public class EntityMutation<E extends Entity> extends KitMutation { final Set<E> entities; final Map<Instant, Set<E>> entitiesByTime; final Map<MatchPlayer, Set<E>> entitiesByPlayer; final Map<E, MatchPlayer> playersByEntity; public EntityMutation(Match match, boolean force) { super(match, force); this.entities = new WeakHashSet<>(); this.entitiesByTime = new WeakHashMap<>(); this.entitiesByPlayer = new WeakHashMap<>(); this.playersByEntity = new WeakHashMap<>(); } /** * Gets an immutable stream of all registered entities. * @return stream of entities. */ public Stream<E> entities() { return ImmutableSet.copyOf(entities).stream(); } /** * Gets an immutable stream of all registered entities * by the time they spawned in ascending order. * @return stream of entities. */ public Stream<E> entitiesByTime() { return ImmutableMap.copyOf(entitiesByTime) .entrySet() .stream() .sorted(Map.Entry.comparingByKey(Comparator.comparing(Instant::toEpochMilli))) .flatMap(entry -> ImmutableSet.copyOf(entry.getValue()).stream()); } /** * Gets an immutable stream of all registered entities * that are owned by a player. * * If the given player is null, this will return entities * with no owner. * @param player the optional player. * @return stream of entities. */ public Stream<E> entitiesByPlayer(@Nullable MatchPlayer player) { return ImmutableSet.copyOf(entitiesByPlayer.getOrDefault(player, new HashSet<>())).stream(); } /** * Gets the optional owner of an entity. * @param entity the entity to find the owner of. * @return the optional player. */ public Optional<MatchPlayer> playerByEntity(Entity entity) { return Optional.ofNullable(playersByEntity.get(entity)); } /** * Are entities with this spawn reason allowed to spawn? * @param reason the spawn reason. * @return whether this reason is allowed. */ public boolean allowed(CreatureSpawnEvent.SpawnReason reason) { switch(reason) { case NATURAL: case DEFAULT: case CHUNK_GEN: case JOCKEY: case MOUNT: return false; default: return true; } } /** * Register a new spawned entity with an optional owner. * @param entity the entity to register. * @param owner the optional owner. * @return the entity. */ public E register(E entity, @Nullable MatchPlayer owner) { entities.add(entity); final Instant now = match().getInstantNow(); final Set<E> byTime = entitiesByTime.getOrDefault(now, new WeakHashSet<>()); byTime.add(entity); entitiesByTime.put(now, byTime); if(owner != null) { final Set<E> byPlayer = entitiesByPlayer.getOrDefault(owner, new WeakHashSet<>()); byPlayer.add(entity); entitiesByPlayer.put(owner, byPlayer); playersByEntity.put(entity, owner); } return entity; } /** * Removes the entity from the world. * * Typically this should be {@link Entity#remove()}, * but it can also expire the entity after a couple of seconds. * @param entity the entity to remove. */ public void remove(E entity) { entity.remove(); } /** * Unregister and remove the given entity. * @param entity the entity. */ public void despawn(E entity) { entities.remove(entity); playersByEntity.remove(entity); Stream.of(entitiesByTime, entitiesByPlayer) .flatMap(map -> ImmutableList.copyOf(map.values()).stream()) .forEach(set -> set.remove(entity)); remove(entity); } /** * Unregister and remove any entities owned by the given player. * @param player the owner of the entities. */ public void despawn(MatchPlayer player) { ImmutableSet.copyOf(entitiesByPlayer.getOrDefault(player, new HashSet<>())).forEach(this::despawn); } /** * Spawn an entity at the given location with no owner. * @see #spawn(Location, Class, MatchPlayer) * @return the entity. */ public E spawn(Location location, Class<E> entityClass) { return spawn(location, entityClass, null); } /** * Spawn an entity at the given location. * @param location the location to spawn the entity. * @param entityClass the class of the entity. * @param owner the optional owner of the entity. * @return the entity. */ public E spawn(Location location, Class<E> entityClass, @Nullable MatchPlayer owner) { E entity = world().spawn(location, entityClass); cast(entity, LivingEntity.class).ifPresent(living -> { living.setCanPickupItems(false); living.setRemoveWhenFarAway(true); EntityEquipment equipment = living.getEquipment(); equipment.setHelmetDropChance(0); equipment.setChestplateDropChance(0); equipment.setLeggingsDropChance(0); equipment.setBootsDropChance(0); }); return register(entity, owner); } @EventHandler(ignoreCancelled = false, priority = EventPriority.HIGHEST) public void onPlayerSpawnEntity(PlayerSpawnEntityEvent event) { match().participant(event.getPlayer()) .ifPresent(player -> cast(event.getEntity(), new TypeToken<E>(){}.getRawType()) .ifPresent(entity -> { register((E) entity, player); event.setCancelled(false); })); } @EventHandler(ignoreCancelled = false, priority = EventPriority.HIGHEST) public void onEntitySpawn(CreatureSpawnEvent event) { boolean allowed = allowed(event.getSpawnReason()); event.setCancelled(!allowed); if(allowed) { cast(event.getEntity(), new TypeToken<E>(){}.getRawType()) .filter(entity -> !playerByEntity((E) entity).isPresent()) .ifPresent(entity -> register((E) entity, null)); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onPlayerDeath(MatchPlayerDeathEvent event) { despawn(event.getVictim()); } @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onPartyChange(PlayerChangePartyEvent event) { despawn(event.getPlayer()); } @Override public void remove(MatchPlayer player) { despawn(player); super.remove(player); } @Override public void disable() { entities().forEach(this::despawn); Stream.of(entitiesByTime, entitiesByPlayer, playersByEntity).forEach(Map::clear); super.disable(); } }