/*
* 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.vanilla.message.processor.play;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector3f;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalCause;
import io.netty.handler.codec.CodecException;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.lanternpowered.server.data.type.LanternNotePitch;
import org.lanternpowered.server.effect.particle.LanternParticleEffect;
import org.lanternpowered.server.effect.particle.LanternParticleType;
import org.lanternpowered.server.game.registry.type.block.BlockRegistryModule;
import org.lanternpowered.server.game.registry.type.item.ItemRegistryModule;
import org.lanternpowered.server.inventory.LanternItemStack;
import org.lanternpowered.server.network.buffer.ByteBuffer;
import org.lanternpowered.server.network.buffer.ByteBufferAllocator;
import org.lanternpowered.server.network.entity.EntityProtocolManager;
import org.lanternpowered.server.network.entity.parameter.ByteBufParameterList;
import org.lanternpowered.server.network.entity.vanilla.EntityParameters;
import org.lanternpowered.server.network.message.Message;
import org.lanternpowered.server.network.message.codec.CodecContext;
import org.lanternpowered.server.network.message.processor.Processor;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutDestroyEntities;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEffect;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityMetadata;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutParticleEffect;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSpawnObject;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutSpawnParticle;
import org.lanternpowered.server.network.vanilla.message.type.play.MessagePlayOutEntityStatus;
import org.spongepowered.api.block.BlockState;
import org.spongepowered.api.block.BlockType;
import org.spongepowered.api.data.key.Keys;
import org.spongepowered.api.data.type.NotePitch;
import org.spongepowered.api.effect.particle.ParticleEffect;
import org.spongepowered.api.effect.particle.ParticleOptions;
import org.spongepowered.api.effect.particle.ParticleTypes;
import org.spongepowered.api.effect.potion.PotionEffectType;
import org.spongepowered.api.effect.potion.PotionEffectTypes;
import org.spongepowered.api.item.ItemType;
import org.spongepowered.api.item.ItemTypes;
import org.spongepowered.api.item.inventory.ItemStackSnapshot;
import org.spongepowered.api.util.Color;
import org.spongepowered.api.util.Direction;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
public final class ProcessorPlayOutParticleEffect implements Processor<MessagePlayOutParticleEffect> {
/**
* Using a cache to bring the amount of operations down for spawning particles.
*/
private final LoadingCache<ParticleEffect, ICachedMessage> cache = Caffeine.newBuilder()
.weakKeys().expireAfterAccess(3, TimeUnit.MINUTES)
.removalListener(this::processRemoval)
.build(this::preProcess);
private final Object2IntMap<PotionEffectType> potionEffectTypeToId = new Object2IntOpenHashMap<>();
{
this.potionEffectTypeToId.defaultReturnValue(0); // Default to water?
this.potionEffectTypeToId.put(PotionEffectTypes.NIGHT_VISION, 5);
this.potionEffectTypeToId.put(PotionEffectTypes.INVISIBILITY, 7);
this.potionEffectTypeToId.put(PotionEffectTypes.JUMP_BOOST, 9);
this.potionEffectTypeToId.put(PotionEffectTypes.FIRE_RESISTANCE, 12);
this.potionEffectTypeToId.put(PotionEffectTypes.SPEED, 14);
this.potionEffectTypeToId.put(PotionEffectTypes.SLOWNESS, 17);
this.potionEffectTypeToId.put(PotionEffectTypes.WATER_BREATHING, 19);
this.potionEffectTypeToId.put(PotionEffectTypes.INSTANT_HEALTH, 21);
this.potionEffectTypeToId.put(PotionEffectTypes.INSTANT_DAMAGE, 23);
this.potionEffectTypeToId.put(PotionEffectTypes.POISON, 25);
this.potionEffectTypeToId.put(PotionEffectTypes.REGENERATION, 28);
this.potionEffectTypeToId.put(PotionEffectTypes.STRENGTH, 31);
this.potionEffectTypeToId.put(PotionEffectTypes.WEAKNESS, 34);
this.potionEffectTypeToId.put(PotionEffectTypes.LUCK, 36);
}
private static int getBlockState(LanternParticleEffect effect, Optional<BlockState> defaultBlockState) {
final Optional<BlockState> blockState = effect.getOption(ParticleOptions.BLOCK_STATE);
if (blockState.isPresent()) {
return BlockRegistryModule.get().getStateInternalIdAndData(blockState.get());
} else {
final Optional<ItemStackSnapshot> optSnapshot = effect.getOption(ParticleOptions.ITEM_STACK_SNAPSHOT);
if (optSnapshot.isPresent()) {
final ItemStackSnapshot snapshot = optSnapshot.get();
final Optional<BlockType> blockType = snapshot.getType().getBlock();
if (blockType.isPresent()) {
// TODO: Item stack data value
return BlockRegistryModule.get().getStateInternalIdAndData(blockType.get().getDefaultState());
} else {
return 0;
}
} else {
return BlockRegistryModule.get().getStateInternalIdAndData(defaultBlockState.get());
}
}
}
private static int getDirectionData(Direction direction) {
if (direction.isSecondaryOrdinal()) {
direction = Direction.getClosest(direction.asOffset(), Direction.Division.ORDINAL);
}
switch (direction) {
case SOUTHEAST:
return 0;
case SOUTH:
return 1;
case SOUTHWEST:
return 2;
case EAST:
return 3;
case WEST:
return 5;
case NORTHEAST:
return 6;
case NORTH:
return 7;
case NORTHWEST:
return 8;
default:
return 4;
}
}
private void processRemoval(@Nullable ParticleEffect key, @Nullable ICachedMessage value, RemovalCause cause) {
if (value instanceof CachedFireworksMessage) {
final ByteBufParameterList parameterList = (ByteBufParameterList) ((CachedFireworksMessage) value)
.entityMetadataMessage.getParameterList();
parameterList.getByteBuffer().ifPresent(ByteBuffer::release);
}
}
private ICachedMessage preProcess(ParticleEffect effect0) {
final LanternParticleEffect effect = (LanternParticleEffect) effect0;
final LanternParticleType type = effect.getType();
final OptionalInt internalType = type.getInternalType();
// Special cases
if (!internalType.isPresent()) {
if (type == ParticleTypes.FIREWORKS) {
// Create the fireworks data item
final LanternItemStack itemStack = new LanternItemStack(ItemTypes.FIREWORKS);
itemStack.tryOffer(Keys.FIREWORK_EFFECTS, effect.getOptionOrDefault(ParticleOptions.FIREWORK_EFFECTS).get());
// Write the item to a parameter list
final ByteBufParameterList parameterList = new ByteBufParameterList(ByteBufferAllocator.unpooled());
parameterList.add(EntityParameters.Fireworks.ITEM, itemStack);
parameterList.getByteBuffer().ifPresent(ByteBuffer::retain);
return new CachedFireworksMessage(new MessagePlayOutEntityMetadata(CachedFireworksMessage.ENTITY_ID, parameterList));
} else if (type == ParticleTypes.FERTILIZER) {
final int quantity = effect.getOptionOrDefault(ParticleOptions.QUANTITY).get();
return new CachedEffectMessage(2005, quantity, false);
} else if (type == ParticleTypes.SPLASH_POTION) {
final int potionId = this.potionEffectTypeToId.getInt(effect.getOptionOrDefault(ParticleOptions.POTION_EFFECT_TYPE).get());
return new CachedEffectMessage(2002, potionId, false);
} else if (type == ParticleTypes.BREAK_BLOCK) {
final int state = getBlockState(effect, type.getDefaultOption(ParticleOptions.BLOCK_STATE));
if (state == 0) {
return EmptyCachedMessage.INSTANCE;
}
return new CachedEffectMessage(2001, state, false);
} else if (type == ParticleTypes.MOBSPAWNER_FLAMES) {
return new CachedEffectMessage(2004, 0, false);
} else if (type == ParticleTypes.ENDER_TELEPORT) {
return new CachedEffectMessage(2003, 0, false);
} else if (type == ParticleTypes.DRAGON_BREATH_ATTACK) {
return new CachedEffectMessage(2006, 0, false);
} else if (type == ParticleTypes.FIRE_SMOKE) {
final Direction direction = effect.getOptionOrDefault(ParticleOptions.DIRECTION).get();
return new CachedEffectMessage(2000, getDirectionData(direction), false);
}
return EmptyCachedMessage.INSTANCE;
}
final int internalId = internalType.getAsInt();
final Vector3f offset = effect.getOption(ParticleOptions.OFFSET).map(Vector3d::toFloat).orElse(Vector3f.ZERO);
final int quantity = effect.getOption(ParticleOptions.QUANTITY).orElse(1);
int[] extra = null;
// The extra values, normal behavior offsetX, offsetY, offsetZ
double f0 = 0f;
double f1 = 0f;
double f2 = 0f;
// Depends on behavior
// Note: If the count > 0 -> speed = 0f else if count = 0 -> speed = 1f
final Optional<BlockState> defaultBlockState;
if (type != ParticleTypes.ITEM_CRACK && (defaultBlockState = type.getDefaultOption(ParticleOptions.BLOCK_STATE)).isPresent()) {
final int state = getBlockState(effect, defaultBlockState);
if (state == 0) {
return EmptyCachedMessage.INSTANCE;
}
extra = new int[] { state };
}
final Optional<ItemStackSnapshot> defaultItemStackSnapshot;
if (extra == null && (defaultItemStackSnapshot = type.getDefaultOption(ParticleOptions.ITEM_STACK_SNAPSHOT)).isPresent()) {
final Optional<ItemStackSnapshot> optItemStackSnapshot = effect.getOption(ParticleOptions.ITEM_STACK_SNAPSHOT);
if (optItemStackSnapshot.isPresent()) {
final ItemStackSnapshot snapshot = optItemStackSnapshot.get();
// TODO: Item damage value
extra = new int[]{ItemRegistryModule.get().getInternalId(snapshot.getType()), 0};
} else {
final Optional<BlockState> optBlockState = effect.getOption(ParticleOptions.BLOCK_STATE);
if (optBlockState.isPresent()) {
final BlockState blockState = optBlockState.get();
final Optional<ItemType> optItemType = blockState.getType().getItem();
if (optItemType.isPresent()) {
// TODO: Item damage value
extra = new int[]{ItemRegistryModule.get().getInternalId(optItemType.get()), 0};
} else {
return EmptyCachedMessage.INSTANCE;
}
} else {
final ItemStackSnapshot snapshot = defaultItemStackSnapshot.get();
extra = new int[]{ItemRegistryModule.get().getInternalId(snapshot.getType()), 0};
}
}
}
if (extra == null) {
extra = new int[0];
}
final Optional<Double> defaultScale = type.getDefaultOption(ParticleOptions.SCALE);
final Optional<Color> defaultColor;
final Optional<NotePitch> defaultNote;
final Optional<Vector3d> defaultVelocity;
if (defaultScale.isPresent()) {
double scale = effect.getOption(ParticleOptions.SCALE).orElse(defaultScale.get());
// The formula of the large explosion acts strange
// Client formula: sizeClient = 1 - sizeServer * 0.5
// The particle effect returns the client value so
// Server formula: sizeServer = (-sizeClient * 2) + 2
if (type == ParticleTypes.LARGE_EXPLOSION || type == ParticleTypes.SWEEP_ATTACK) {
scale = (-scale * 2f) + 2f;
}
if (scale == 0f) {
return new CachedParticleMessage(internalId, offset, quantity, extra);
}
f0 = scale;
} else if ((defaultColor = type.getDefaultOption(ParticleOptions.COLOR)).isPresent()) {
final boolean isSpell = type == ParticleTypes.MOB_SPELL || type == ParticleTypes.AMBIENT_MOB_SPELL;
Color color = effect.getOption(ParticleOptions.COLOR).orElse(null);
if (!isSpell && (color == null || color.equals(defaultColor.get()))) {
return new CachedParticleMessage(internalId, offset, quantity, extra);
} else if (isSpell && color == null) {
color = defaultColor.get();
}
f0 = color.getRed() / 255f;
f1 = color.getGreen() / 255f;
f2 = color.getBlue() / 255f;
// Make sure that the x and z component are never 0 for these effects,
// they would trigger the slow horizontal velocity (unsupported on the server),
// but we already chose for the color, can't have both
if (isSpell) {
f0 = Math.max(f0, 0.001f);
f2 = Math.max(f0, 0.001f);
}
// If the f0 value 0 is, the redstone will set it automatically to red 255
if (f0 == 0f && type == ParticleTypes.REDSTONE_DUST) {
f0 = 0.00001f;
}
} else if ((defaultNote = type.getDefaultOption(ParticleOptions.NOTE)).isPresent()) {
final NotePitch notePitch = effect.getOption(ParticleOptions.NOTE).orElse(defaultNote.get());
final float note = ((LanternNotePitch) notePitch).getInternalId();
if (note == 0f) {
return new CachedParticleMessage(internalId, offset, quantity, extra);
}
f0 = note / 24f;
} else if ((defaultVelocity = type.getDefaultOption(ParticleOptions.VELOCITY)).isPresent()) {
final Vector3d velocity = effect.getOption(ParticleOptions.VELOCITY).orElse(defaultVelocity.get());
f0 = velocity.getX();
f1 = velocity.getY();
f2 = velocity.getZ();
final Optional<Boolean> slowHorizontalVelocity = type.getDefaultOption(ParticleOptions.SLOW_HORIZONTAL_VELOCITY);
if (slowHorizontalVelocity.isPresent() &&
effect.getOption(ParticleOptions.SLOW_HORIZONTAL_VELOCITY).orElse(slowHorizontalVelocity.get())) {
f0 = 0f;
f2 = 0f;
}
// The y value won't work for this effect, if the value isn't 0 the velocity won't work
if (type == ParticleTypes.WATER_SPLASH) {
f1 = 0f;
}
if (f0 == 0f && f1 == 0f && f2 == 0f) {
return new CachedParticleMessage(internalId, offset, quantity, extra);
}
}
// Is this check necessary?
if (f0 == 0f && f1 == 0f && f2 == 0f) {
return new CachedParticleMessage(internalId, offset, quantity, extra);
}
return new CachedOffsetParticleMessage(internalId, new Vector3f(f0, f1, f2), offset, quantity, extra);
}
@Override
public void process(CodecContext context, MessagePlayOutParticleEffect message, List<Message> output) throws CodecException {
final ICachedMessage cached = this.cache.get(message.getParticleEffect());
cached.process(message.getPosition(), output);
}
private static final class EmptyCachedMessage implements ICachedMessage {
public static final EmptyCachedMessage INSTANCE = new EmptyCachedMessage();
@Override
public void process(Vector3d position, List<Message> output) {
}
}
private static final class CachedFireworksMessage implements ICachedMessage {
// Get the next free entity id
private static final int ENTITY_ID;
private static final UUID UNIQUE_ID;
private static final MessagePlayOutDestroyEntities DESTROY_ENTITY;
private static final MessagePlayOutEntityStatus TRIGGER_EFFECT;
static {
ENTITY_ID = EntityProtocolManager.acquireEntityId();
UNIQUE_ID = UUID.randomUUID();
DESTROY_ENTITY = new MessagePlayOutDestroyEntities(ENTITY_ID);
// The status index that is used to trigger the fireworks effect
TRIGGER_EFFECT = new MessagePlayOutEntityStatus(ENTITY_ID, 17);
}
private final MessagePlayOutEntityMetadata entityMetadataMessage;
private CachedFireworksMessage(MessagePlayOutEntityMetadata entityMetadataMessage) {
this.entityMetadataMessage = entityMetadataMessage;
}
@Override
public void process(Vector3d position, List<Message> output) {
// 76 -> The internal id used to spawn fireworks
output.add(new MessagePlayOutSpawnObject(ENTITY_ID, UNIQUE_ID, 76, 0, position, 0, 0, Vector3d.ZERO));
output.add(this.entityMetadataMessage);
output.add(TRIGGER_EFFECT);
output.add(DESTROY_ENTITY);
}
}
private static final class CachedParticleMessage implements ICachedMessage {
private final int particleId;
private final Vector3f offsetData;
private final int count;
private final int[] extra;
private CachedParticleMessage(int particleId, Vector3f offsetData, int count, int[] extra) {
this.particleId = particleId;
this.offsetData = offsetData;
this.count = count;
this.extra = extra;
}
@Override
public void process(Vector3d position, List<Message> output) {
output.add(new MessagePlayOutSpawnParticle(this.particleId, position.toFloat(),
this.offsetData, 0f, this.count, this.extra));
}
}
private static final class CachedOffsetParticleMessage implements ICachedMessage {
private final int particleId;
private final Vector3f offsetData;
private final Vector3f offset;
private final int count;
private final int[] extra;
private CachedOffsetParticleMessage(int particleId, Vector3f offsetData, Vector3f offset, int count, int[] extra) {
this.particleId = particleId;
this.offsetData = offsetData;
this.offset = offset;
this.count = count;
this.extra = extra;
}
@Override
public void process(Vector3d position, List<Message> output) {
final Random random = new Random();
if (this.offset.equals(Vector3f.ZERO)) {
final MessagePlayOutSpawnParticle message = new MessagePlayOutSpawnParticle(
this.particleId, position.toFloat(), this.offsetData, 1f, 0, this.extra);
for (int i = 0; i < this.count; i++) {
output.add(message);
}
} else {
final float px = (float) position.getX();
final float py = (float) position.getY();
final float pz = (float) position.getZ();
final float ox = this.offset.getX();
final float oy = this.offset.getY();
final float oz = this.offset.getZ();
for (int i = 0; i < this.count; i++) {
final double px0 = px + (random.nextFloat() * 2f - 1f) * ox;
final double py0 = py + (random.nextFloat() * 2f - 1f) * oy;
final double pz0 = pz + (random.nextFloat() * 2f - 1f) * oz;
output.add(new MessagePlayOutSpawnParticle(this.particleId, new Vector3f(px0, py0, pz0),
this.offsetData, 1f, 0, this.extra));
}
}
}
}
private static final class CachedEffectMessage implements ICachedMessage {
private final int type;
private final int data;
private final boolean broadcast;
private CachedEffectMessage(int type, int data, boolean broadcast) {
this.broadcast = broadcast;
this.type = type;
this.data = data;
}
@Override
public void process(Vector3d position, List<Message> output) {
output.add(new MessagePlayOutEffect(position.round().toInt(), this.type, this.data, this.broadcast));
}
}
private interface ICachedMessage {
void process(Vector3d position, List<Message> output);
}
}