9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-31 21:06:31 +00:00

合并上下文

This commit is contained in:
XiaoMoMi
2025-05-14 15:54:02 +08:00
parent 8cc83d569c
commit 60e4db7a9a
23 changed files with 353 additions and 157 deletions

View File

@@ -6,6 +6,7 @@ import net.momirealms.craftengine.bukkit.nms.FastNMS;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.bukkit.world.BukkitBlockInWorld;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.properties.Property;
@@ -14,7 +15,9 @@ import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.context.ContextHolder;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.event.EventTrigger;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
@@ -136,7 +139,13 @@ public class BlockEventListener implements Listener {
return;
}
// execute functions
net.momirealms.craftengine.core.world.World world = new BukkitWorld(location.getWorld());
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.BLOCK, new BukkitBlockInWorld(block))
.withParameter(DirectContextParameters.BLOCK_STATE, state));
state.owner().value().execute(context, EventTrigger.BREAK);
// handle waterlogged blocks
@SuppressWarnings("unchecked")
Property<Boolean> waterloggedProperty = (Property<Boolean>) state.owner().value().getProperty("waterlogged");
@@ -146,6 +155,7 @@ public class BlockEventListener implements Listener {
location.getWorld().setBlockData(location, Material.WATER.createBlockData());
}
}
// play sound
WorldPosition position = new WorldPosition(world, location.getBlockX() + 0.5, location.getBlockY() + 0.5, location.getBlockZ() + 0.5);
world.playBlockSound(position, state.sounds().breakSound());

View File

@@ -25,7 +25,7 @@ import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.event.BlockEventFunctions;
import net.momirealms.craftengine.core.plugin.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
@@ -459,7 +459,7 @@ public class BukkitBlockManager extends AbstractBlockManager {
}
Object eventsObj = ResourceConfigUtils.get(section, "events", "event");
EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events = parseEvents(eventsObj);
EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events = EventFunctions.parseEvents(eventsObj);
Map<String, Object> behaviors = MiscUtils.castToMap(section.getOrDefault("behavior", Map.of()), false);
CustomBlock block = BukkitCustomBlock.builder(id)
@@ -513,55 +513,6 @@ public class BukkitBlockManager extends AbstractBlockManager {
}
}
private EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> parseEvents(Object eventsObj) {
EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events = new EnumMap<>(EventTrigger.class);
if (eventsObj instanceof Map<?, ?> eventsSection) {
Map<String, Object> eventsSectionMap = MiscUtils.castToMap(eventsSection, false);
for (Map.Entry<String, Object> eventEntry : eventsSectionMap.entrySet()) {
try {
EventTrigger eventTrigger = EventTrigger.byName(eventEntry.getKey());
if (eventEntry.getValue() instanceof List<?> list) {
if (list.size() == 1) {
events.put(eventTrigger, List.of(BlockEventFunctions.fromMap(MiscUtils.castToMap(list.get(0), false))));
} else if (list.size() == 2) {
events.put(eventTrigger, List.of(
BlockEventFunctions.fromMap(MiscUtils.castToMap(list.get(0), false)),
BlockEventFunctions.fromMap(MiscUtils.castToMap(list.get(1), false))
));
} else {
List<Function<PlayerOptionalContext>> eventsList = new ArrayList<>();
for (Object event : list) {
eventsList.add(BlockEventFunctions.fromMap(MiscUtils.castToMap(event, false)));
}
events.put(eventTrigger, eventsList);
}
} else if (eventEntry.getValue() instanceof Map<?, ?> eventSection) {
events.put(eventTrigger, List.of(BlockEventFunctions.fromMap(MiscUtils.castToMap(eventSection, false))));
}
} catch (IllegalArgumentException e) {
throw new LocalizedResourceConfigException("warning.config.event.invalid_trigger", eventEntry.getKey());
}
}
} else if (eventsObj instanceof List<?> list) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> eventsList = (List<Map<String, Object>>) list;
for (Map<String, Object> eventSection : eventsList) {
Object onObj = eventSection.get("on");
if (onObj == null) {
throw new LocalizedResourceConfigException("warning.config.event.missing_trigger");
}
try {
EventTrigger eventTrigger = EventTrigger.byName(onObj.toString());
Function<PlayerOptionalContext> function = BlockEventFunctions.fromMap(eventSection);
events.computeIfAbsent(eventTrigger, k -> new ArrayList<>(4)).add(function);
} catch (IllegalArgumentException e) {
throw new LocalizedResourceConfigException("warning.config.event.invalid_trigger", onObj.toString());
}
}
}
return events;
}
private Map<String, Property<?>> parseProperties(Map<String, Object> propertiesSection) {
Map<String, Property<?>> properties = new HashMap<>();
for (Map.Entry<String, Object> entry : propertiesSection.entrySet()) {

View File

@@ -16,10 +16,15 @@ import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.sound.SoundData;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.world.WorldPosition;
import org.bukkit.*;
@@ -212,7 +217,8 @@ public class BukkitFurnitureManager extends AbstractFurnitureManager {
// get loot table
LootTable<ItemStack> lootTable = lootMap == null ? null : LootTable.fromMap(lootMap);
CustomFurniture furniture = new CustomFurniture(id, settings, placements, lootTable);
EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events = EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event"));
CustomFurniture furniture = new CustomFurniture(id, settings, placements, events, lootTable);
byId.put(id, furniture);
}
}

View File

@@ -6,14 +6,15 @@ import net.momirealms.craftengine.bukkit.util.MaterialUtils;
import net.momirealms.craftengine.core.item.*;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.modifier.ItemDataModifier;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.event.EventTrigger;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
public class BukkitCustomItem implements CustomItem<ItemStack> {
private final Key id;
@@ -26,6 +27,7 @@ public class BukkitCustomItem implements CustomItem<ItemStack> {
private final NetworkItemDataProcessor<ItemStack>[] networkItemDataProcessors;
private final List<ItemBehavior> behaviors;
private final ItemSettings settings;
private final EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events;
@SuppressWarnings("unchecked")
public BukkitCustomItem(Key id,
@@ -34,10 +36,12 @@ public class BukkitCustomItem implements CustomItem<ItemStack> {
List<ItemDataModifier<ItemStack>> modifiers,
List<ItemDataModifier<ItemStack>> clientBoundModifiers,
List<ItemBehavior> behaviors,
ItemSettings settings) {
ItemSettings settings,
EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events) {
this.id = id;
this.material = material;
this.materialKey = materialKey;
this.events = events;
// unchecked cast
this.modifiers = modifiers.toArray(new ItemDataModifier[0]);
// unchecked cast
@@ -65,6 +69,13 @@ public class BukkitCustomItem implements CustomItem<ItemStack> {
this.networkItemDataProcessors = networkItemDataProcessors.toArray(new NetworkItemDataProcessor[0]);
}
@Override
public void execute(PlayerOptionalContext context, EventTrigger trigger) {
for (Function<PlayerOptionalContext> function : Optional.ofNullable(this.events.get(trigger)).orElse(Collections.emptyList())) {
function.run(context);
}
}
@Override
public Key id() {
return this.id;
@@ -145,6 +156,7 @@ public class BukkitCustomItem implements CustomItem<ItemStack> {
private Material material;
private Key materialKey;
private ItemSettings settings;
private EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events = new EnumMap<>(EventTrigger.class);
private final List<ItemBehavior> behaviors = new ArrayList<>();
private final List<ItemDataModifier<ItemStack>> modifiers = new ArrayList<>();
private final List<ItemDataModifier<ItemStack>> clientBoundModifiers = new ArrayList<>();
@@ -204,10 +216,16 @@ public class BukkitCustomItem implements CustomItem<ItemStack> {
return this;
}
@Override
public Builder<ItemStack> events(EnumMap<EventTrigger, List<Function<PlayerOptionalContext>>> events) {
this.events = events;
return this;
}
@Override
public CustomItem<ItemStack> build() {
this.modifiers.addAll(this.settings.modifiers());
return new BukkitCustomItem(this.id, this.materialKey, this.material, this.modifiers, this.clientBoundModifiers, this.behaviors, this.settings);
return new BukkitCustomItem(this.id, this.materialKey, this.material, this.modifiers, this.clientBoundModifiers, this.behaviors, this.settings, this.events);
}
}
}

View File

@@ -33,6 +33,7 @@ import net.momirealms.craftengine.core.pack.model.select.TrimMaterialSelectPrope
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.context.ContextHolder;
import net.momirealms.craftengine.core.plugin.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
@@ -357,6 +358,7 @@ public class BukkitItemManager extends AbstractItemManager<ItemStack> {
itemSettings.canPlaceRelatedVanillaBlock(true);
}
itemBuilder.settings(itemSettings);
itemBuilder.events(EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event")));
CustomItem<ItemStack> customItem = itemBuilder.build();
customItems.put(id, customItem);

View File

@@ -247,12 +247,16 @@ public class ComponentItemFactory1_20_5 extends BukkitItemFactory<ComponentItemW
@Override
protected Optional<Integer> dyedColor(ComponentItemWrapper item) {
if (!item.hasComponent(ComponentTypes.DYED_COLOR)) return Optional.empty();
return Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentTypes.DYED_COLOR,
item.getComponent(ComponentTypes.DYED_COLOR)
).orElse(null)
);
Object javaObj = ComponentType.encodeJava(
ComponentTypes.DYED_COLOR,
item.getComponent(ComponentTypes.DYED_COLOR)
).orElse(null);
if (javaObj instanceof Integer integer) {
return Optional.of(integer);
} else if (javaObj instanceof Map<?, ?> map) {
return Optional.of((int) map.get("rgb"));
}
return Optional.empty();
}
@Override

View File

@@ -14,6 +14,7 @@ import net.momirealms.craftengine.core.item.CustomItem;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.plugin.context.CommonParameterProvider;
import net.momirealms.craftengine.core.plugin.context.ContextHolder;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
@@ -33,6 +34,7 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
@@ -97,11 +99,10 @@ public class ItemEventListener implements Listener {
// run custom functions
CustomBlock customBlock = immutableBlockState.owner().value();
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.PLAYER, serverPlayer)
.withParameter(DirectContextParameters.BLOCK, new BukkitBlockInWorld(block))
.withParameter(DirectContextParameters.BLOCK_STATE, immutableBlockState)
.withParameter(DirectContextParameters.CLICK_TYPE, action.isRightClick() ? ClickType.RIGHT : ClickType.LEFT)
.build());
.withParameter(DirectContextParameters.HAND, hand)
.withParameter(DirectContextParameters.CLICK_TYPE, action.isRightClick() ? ClickType.RIGHT : ClickType.LEFT));
customBlock.execute(context, EventTrigger.CLICK);
if (action.isRightClick()) customBlock.execute(context, EventTrigger.RIGHT_CLICK);
else customBlock.execute(context, EventTrigger.LEFT_CLICK);
@@ -156,6 +157,18 @@ public class ItemEventListener implements Listener {
}
}
// execute item functions
if (hasCustomItem) {
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.BLOCK, new BukkitBlockInWorld(block))
.withOptionalParameter(DirectContextParameters.BLOCK_STATE, immutableBlockState)
.withParameter(DirectContextParameters.HAND, hand)
.withParameter(DirectContextParameters.CLICK_TYPE, ClickType.RIGHT));
CustomItem<ItemStack> customItem = optionalCustomItem.get();
customItem.execute(context, EventTrigger.CLICK);
customItem.execute(context, EventTrigger.RIGHT_CLICK);
}
// 检查其他的物品行为,物品行为理论只在交互时处理
Optional<List<ItemBehavior>> optionalItemBehaviors = itemInHand.getItemBehavior();
// 物品类型是否包含自定义物品行为,行为不一定来自于自定义物品,部分原版物品也包含了新的行为
@@ -181,42 +194,83 @@ public class ItemEventListener implements Listener {
}
}
}
if (hasCustomItem && action == Action.LEFT_CLICK_BLOCK) {
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.BLOCK, new BukkitBlockInWorld(block))
.withOptionalParameter(DirectContextParameters.BLOCK_STATE, immutableBlockState)
.withParameter(DirectContextParameters.HAND, hand)
.withParameter(DirectContextParameters.CLICK_TYPE, ClickType.LEFT));
CustomItem<ItemStack> customItem = optionalCustomItem.get();
customItem.execute(context, EventTrigger.CLICK);
customItem.execute(context, EventTrigger.LEFT_CLICK);
}
}
@EventHandler
public void onInteractAir(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR)
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_AIR && action != Action.LEFT_CLICK_AIR)
return;
Player bukkitPlayer = event.getPlayer();
BukkitServerPlayer player = this.plugin.adapt(bukkitPlayer);
InteractionHand hand = event.getHand() == EquipmentSlot.HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
if (cancelEventIfHasInteraction(event, player, hand)) {
Player player = event.getPlayer();
BukkitServerPlayer serverPlayer = this.plugin.adapt(player);
if (serverPlayer.isSpectatorMode())
return;
}
if (player.isSpectatorMode()) {
return;
}
// Gets the item in hand
Item<ItemStack> itemInHand = player.getItemInHand(hand);
InteractionHand hand = event.getHand() == EquipmentSlot.HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
Item<ItemStack> itemInHand = serverPlayer.getItemInHand(hand);
// should never be null
if (itemInHand == null) return;
Optional<List<ItemBehavior>> optionalItemBehaviors = itemInHand.getItemBehavior();
if (optionalItemBehaviors.isPresent()) {
for (ItemBehavior itemBehavior : optionalItemBehaviors.get()) {
InteractionResult result = itemBehavior.use(player.world(), player, hand);
if (result == InteractionResult.SUCCESS_AND_CANCEL) {
event.setCancelled(true);
return;
}
if (result != InteractionResult.PASS) {
return;
if (cancelEventIfHasInteraction(event, serverPlayer, hand)) {
return;
}
Optional<CustomItem<ItemStack>> optionalCustomItem = itemInHand.getCustomItem();
if (optionalCustomItem.isPresent()) {
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.HAND, hand)
.withParameter(DirectContextParameters.CLICK_TYPE, action.isRightClick() ? ClickType.RIGHT : ClickType.LEFT));
CustomItem<ItemStack> customItem = optionalCustomItem.get();
customItem.execute(context, EventTrigger.CLICK);
if (action.isRightClick()) customItem.execute(context, EventTrigger.RIGHT_CLICK);
else customItem.execute(context, EventTrigger.LEFT_CLICK);
}
if (action.isRightClick()) {
Optional<List<ItemBehavior>> optionalItemBehaviors = itemInHand.getItemBehavior();
if (optionalItemBehaviors.isPresent()) {
for (ItemBehavior itemBehavior : optionalItemBehaviors.get()) {
InteractionResult result = itemBehavior.use(serverPlayer.world(), serverPlayer, hand);
if (result == InteractionResult.SUCCESS_AND_CANCEL) {
event.setCancelled(true);
return;
}
if (result != InteractionResult.PASS) {
return;
}
}
}
}
}
@EventHandler(ignoreCancelled = true)
public void onConsumeItem(PlayerItemConsumeEvent event) {
ItemStack consumedItem = event.getItem();
if (ItemUtils.isEmpty(consumedItem)) return;
Item<ItemStack> wrapped = this.plugin.itemManager().wrap(consumedItem);
Optional<CustomItem<ItemStack>> optionalCustomItem = wrapped.getCustomItem();
if (optionalCustomItem.isEmpty()) {
return;
}
CustomItem<ItemStack> customItem = optionalCustomItem.get();
PlayerOptionalContext context = PlayerOptionalContext.of(this.plugin.adapt(event.getPlayer()), ContextHolder.builder()
.withParameter(DirectContextParameters.CONSUMED_ITEM, wrapped)
.withParameter(DirectContextParameters.HAND, event.getHand() == EquipmentSlot.HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND)
);
customItem.execute(context, EventTrigger.CONSUME);
}
private boolean cancelEventIfHasInteraction(PlayerInteractEvent event, BukkitServerPlayer player, InteractionHand hand) {
if (hand == InteractionHand.OFF_HAND) {
int currentTicks = player.gameTicks();

View File

@@ -33,12 +33,13 @@ import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData;
import net.momirealms.craftengine.core.pack.host.ResourcePackHost;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.context.ContextHolder;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.network.*;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.craftengine.core.world.BlockHitResult;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.EntityHitResult;
import net.momirealms.craftengine.core.world.WorldEvents;
import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.Palette;
import net.momirealms.craftengine.core.world.chunk.PalettedContainer;
import net.momirealms.craftengine.core.world.chunk.packet.BlockEntityData;
@@ -46,6 +47,7 @@ import net.momirealms.craftengine.core.world.chunk.packet.MCSection;
import net.momirealms.craftengine.core.world.collision.AABB;
import net.momirealms.sparrow.nbt.Tag;
import org.bukkit.*;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
@@ -1688,6 +1690,16 @@ public class PacketConsumers {
if (EventUtils.fireAndCheckCancel(breakEvent)) {
return;
}
// execute functions
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.FURNITURE, furniture)
.withParameter(DirectContextParameters.POSITION, new WorldPosition(furniture.world(), furniture.position()))
.withParameter(DirectContextParameters.CLICK_TYPE, ClickType.LEFT));
furniture.config().execute(context, EventTrigger.LEFT_CLICK);
furniture.config().execute(context, EventTrigger.CLICK);
furniture.config().execute(context, EventTrigger.BREAK);
CraftEngineFurniture.remove(furniture, serverPlayer, !serverPlayer.isCreativeMode(), true);
}
} else if (actionType == Reflections.instance$ServerboundInteractPacket$ActionType$INTERACT_AT) {
@@ -1710,6 +1722,14 @@ public class PacketConsumers {
return;
}
// execute functions
PlayerOptionalContext context = PlayerOptionalContext.of(serverPlayer, ContextHolder.builder()
.withParameter(DirectContextParameters.FURNITURE, furniture)
.withParameter(DirectContextParameters.POSITION, new WorldPosition(furniture.world(), furniture.position()))
.withParameter(DirectContextParameters.CLICK_TYPE, ClickType.RIGHT));
furniture.config().execute(context, EventTrigger.RIGHT_CLICK);
furniture.config().execute(context, EventTrigger.CLICK);
if (player.isSneaking()) {
// try placing another furniture above it
AABB hitBox = furniture.aabbByEntityId(entityId);