diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/data/PlayerData.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/data/PlayerData.java new file mode 100644 index 000000000..202f1b5a7 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/data/PlayerData.java @@ -0,0 +1,16 @@ +package net.momirealms.craftengine.bukkit.entity.data; + +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections; + +public class PlayerData extends LivingEntityData { + public static final PlayerData PlayerAbsorption = new PlayerData<>(PlayerData.class, EntityDataValue.Serializers$FLOAT, 0.0f); + public static final PlayerData Score = new PlayerData<>(PlayerData.class, EntityDataValue.Serializers$INT, 0); + public static final PlayerData PlayerModeCustomisation = new PlayerData<>(PlayerData.class, EntityDataValue.Serializers$BYTE, (byte) 0); + public static final PlayerData PlayerMainHand = new PlayerData<>(PlayerData.class, EntityDataValue.Serializers$BYTE, (byte) 1); + public static final PlayerData ShoulderLeft = new PlayerData<>(PlayerData.class, EntityDataValue.Serializers$COMPOUND_TAG, CoreReflections.instance$CompoundTag$Empty); + public static final PlayerData ShoulderRight = new PlayerData<>(PlayerData.class, EntityDataValue.Serializers$COMPOUND_TAG, CoreReflections.instance$CompoundTag$Empty); + + public PlayerData(Class clazz, Object serializer, T defaultValue) { + super(clazz, serializer, defaultValue); + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurniture.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurniture.java index 6c8c3d4d3..97daff287 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurniture.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurniture.java @@ -3,11 +3,15 @@ package net.momirealms.craftengine.bukkit.entity.furniture; import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import net.momirealms.craftengine.bukkit.entity.BukkitEntity; +import net.momirealms.craftengine.bukkit.entity.furniture.seat.BukkitSeatEntity; import net.momirealms.craftengine.bukkit.nms.FastNMS; +import net.momirealms.craftengine.bukkit.plugin.network.BukkitNetworkManager; import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections; +import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer; import net.momirealms.craftengine.bukkit.util.EntityUtils; import net.momirealms.craftengine.bukkit.util.LegacyAttributeUtils; import net.momirealms.craftengine.bukkit.util.LocationUtils; +import net.momirealms.craftengine.bukkit.util.PlayerUtils; import net.momirealms.craftengine.core.entity.furniture.*; import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.util.ArrayUtils; @@ -17,6 +21,7 @@ import net.momirealms.craftengine.core.util.VersionHelper; import net.momirealms.craftengine.core.world.WorldPosition; import net.momirealms.craftengine.core.world.collision.AABB; import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.attribute.Attribute; import org.bukkit.entity.*; import org.bukkit.persistence.PersistentDataType; @@ -28,6 +33,7 @@ import org.joml.Vector3f; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.*; +import java.util.function.Consumer; public class BukkitFurniture implements Furniture { private final Key id; @@ -50,7 +56,7 @@ public class BukkitFurniture implements Furniture { private final boolean hasExternalModel; // seats private final Set occupiedSeats = Collections.synchronizedSet(new HashSet<>()); - private final Vector> seats = new Vector<>(); + private final Map seats = Collections.synchronizedMap(new HashMap<>()); // cached spawn packet private Object cachedSpawnPacket; private Object cachedMinimizedSpawnPacket; @@ -190,24 +196,13 @@ public class BukkitFurniture implements Furniture { if (entity != null) entity.destroy(); } - for (WeakReference r : this.seats) { - Entity entity = r.get(); - if (entity == null) continue; - for (Entity passenger : entity.getPassengers()) { - entity.removePassenger(passenger); - } - entity.remove(); - } - this.seats.clear(); + destroySeats(); } @Override public void destroySeats() { - for (WeakReference entity : this.seats) { - Entity e = entity.get(); - if (e != null) { - e.remove(); - } + for (BukkitSeatEntity seat : this.seats.values()) { + seat.destroy(); } this.seats.clear(); } @@ -295,7 +290,17 @@ public class BukkitFurniture implements Furniture { @Override public void spawnSeatEntityForPlayer(net.momirealms.craftengine.core.entity.player.Player player, Seat seat) { - spawnSeatEntityForPlayer((Player) player.platformPlayer(), seat); + BukkitSeatEntity seatEntity = (BukkitSeatEntity) seat.spawn(player, this); + if (seatEntity == null) { + this.removeOccupiedSeat(seat.offset()); + return; + } + this.seats.put(seatEntity.playerID(), seatEntity); + player.setSeat(seatEntity); + BukkitServerPlayer serverPlayer = (BukkitServerPlayer) player; + for (Player p : PlayerUtils.getTrackedBy(serverPlayer.platformPlayer())) { + BukkitNetworkManager.instance().getOnlineUser(p).entityPacketHandlers().put(seatEntity.playerID(), seatEntity); + } } @Override @@ -318,10 +323,31 @@ public class BukkitFurniture implements Furniture { } } - private void spawnSeatEntityForPlayer(org.bukkit.entity.Player player, Seat seat) { - Location location = this.calculateSeatLocation(seat); - Entity seatEntity = seat.limitPlayerRotation() ? - EntityUtils.spawnEntity(player.getWorld(), VersionHelper.isOrAbove1_20_2() ? location.subtract(0,0.9875,0) : location.subtract(0,0.990625,0), EntityType.ARMOR_STAND, entity -> { + public Location calculateSeatLocation(Seat seat) { + Vector3f offset = QuaternionUtils.toQuaternionf(0, Math.toRadians(180 - this.location.getYaw()), 0).conjugate().transform(new Vector3f(seat.offset())); + double yaw = seat.yaw() + this.location.getYaw(); + if (yaw < -180) yaw += 360; + Location newLocation = this.location.clone(); + newLocation.setYaw((float) yaw); + newLocation.add(offset.x, offset.y + 0.6, -offset.z); + return newLocation; + } + + public BukkitSeatEntity seatByPlayerId(int playerId) { + return this.seats.get(playerId); + } + + public void removeSeatEntity(int playerId) { + this.seats.remove(playerId); + } + + public static Entity spawnSeatEntity(Furniture furniture, World world, Location loc, boolean limitPlayerRotation, Consumer function) { + EntityType type; + if (limitPlayerRotation) { + type = EntityType.ARMOR_STAND; + loc = VersionHelper.isOrAbove1_20_2() ? loc.subtract(0,0.9875,0) : loc.subtract(0,0.990625,0); + if (function == null) { + function = entity -> { ArmorStand armorStand = (ArmorStand) entity; if (VersionHelper.isOrAbove1_21_3()) { Objects.requireNonNull(armorStand.getAttribute(Attribute.MAX_HEALTH)).setBaseValue(0.01); @@ -337,29 +363,22 @@ public class BukkitFurniture implements Furniture { armorStand.setAI(false); armorStand.setGravity(false); armorStand.setPersistent(false); - armorStand.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER, this.baseEntityId()); - armorStand.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING, seat.offset().x + ", " + seat.offset().y + ", " + seat.offset().z); - }) : - EntityUtils.spawnEntity(player.getWorld(), VersionHelper.isOrAbove1_20_2() ? location : location.subtract(0,0.25,0), EntityType.ITEM_DISPLAY, entity -> { + armorStand.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER, furniture.baseEntityId()); + //armorStand.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING, seat.offset().x + ", " + seat.offset().y + ", " + seat.offset().z); + }; + } + } else { + type = EntityType.ITEM_DISPLAY; + loc = VersionHelper.isOrAbove1_20_2() ? loc : loc.subtract(0,0.25,0); + if (function == null) { + function = entity -> { ItemDisplay itemDisplay = (ItemDisplay) entity; itemDisplay.setPersistent(false); - itemDisplay.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER, this.baseEntityId()); - itemDisplay.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING, seat.offset().x + ", " + seat.offset().y + ", " + seat.offset().z); - }); - this.seats.add(new WeakReference<>(seatEntity)); - if (!seatEntity.addPassenger(player)) { - seatEntity.remove(); - this.removeOccupiedSeat(seat.offset()); + itemDisplay.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER, furniture.baseEntityId()); + //itemDisplay.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING, seat.offset().x + ", " + seat.offset().y + ", " + seat.offset().z); + }; + } } - } - - private Location calculateSeatLocation(Seat seat) { - Vector3f offset = QuaternionUtils.toQuaternionf(0, Math.toRadians(180 - this.location.getYaw()), 0).conjugate().transform(new Vector3f(seat.offset())); - double yaw = seat.yaw() + this.location.getYaw(); - if (yaw < -180) yaw += 360; - Location newLocation = this.location.clone(); - newLocation.setYaw((float) yaw); - newLocation.add(offset.x, offset.y + 0.6, -offset.z); - return newLocation; + return EntityUtils.spawnEntity(world, loc, type, function); } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurnitureManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurnitureManager.java index 1a1744ced..db1e88b7c 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurnitureManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/BukkitFurnitureManager.java @@ -1,6 +1,8 @@ package net.momirealms.craftengine.bukkit.entity.furniture; +import net.momirealms.craftengine.bukkit.api.BukkitAdaptors; import net.momirealms.craftengine.bukkit.entity.furniture.hitbox.InteractionHitBox; +import net.momirealms.craftengine.bukkit.entity.furniture.seat.BukkitSeatEntity; import net.momirealms.craftengine.bukkit.nms.CollisionEntity; import net.momirealms.craftengine.bukkit.nms.FastNMS; import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; @@ -10,10 +12,10 @@ import net.momirealms.craftengine.bukkit.util.EntityUtils; import net.momirealms.craftengine.bukkit.util.KeyUtils; import net.momirealms.craftengine.bukkit.util.LocationUtils; import net.momirealms.craftengine.core.entity.furniture.*; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; import net.momirealms.craftengine.core.plugin.config.Config; 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.VersionHelper; import net.momirealms.craftengine.core.world.WorldPosition; import org.bukkit.*; @@ -23,7 +25,6 @@ import org.bukkit.event.Listener; import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.joml.Vector3f; import java.io.IOException; import java.util.Collection; @@ -37,7 +38,6 @@ public class BukkitFurnitureManager extends AbstractFurnitureManager { public static final NamespacedKey FURNITURE_KEY = KeyUtils.toNamespacedKey(FurnitureManager.FURNITURE_KEY); public static final NamespacedKey FURNITURE_EXTRA_DATA_KEY = KeyUtils.toNamespacedKey(FurnitureManager.FURNITURE_EXTRA_DATA_KEY); public static final NamespacedKey FURNITURE_SEAT_BASE_ENTITY_KEY = KeyUtils.toNamespacedKey(FurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY); - public static final NamespacedKey FURNITURE_SEAT_VECTOR_3F_KEY = KeyUtils.toNamespacedKey(FurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY); public static final NamespacedKey FURNITURE_COLLISION = KeyUtils.toNamespacedKey(FurnitureManager.FURNITURE_COLLISION); public static Class COLLISION_ENTITY_CLASS = Interaction.class; public static Object NMS_COLLISION_ENTITY_TYPE = MEntityTypes.INTERACTION; @@ -336,20 +336,32 @@ public class BukkitFurnitureManager extends AbstractFurnitureManager { } protected void tryLeavingSeat(@NotNull Player player, @NotNull Entity vehicle) { - Integer baseFurniture = vehicle.getPersistentDataContainer().get(FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER); - if (baseFurniture == null) return; - vehicle.remove(); - BukkitFurniture furniture = loadedFurnitureByRealEntityId(baseFurniture); + net.momirealms.craftengine.core.entity.player.Player serverPlayer = BukkitAdaptors.adapt(player); + if (serverPlayer == null) { + Integer baseFurniture = vehicle.getPersistentDataContainer().get(FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER); + if (baseFurniture == null) return; + BukkitFurniture furniture = loadedFurnitureByRealEntityId(baseFurniture); + if (furniture == null) { + vehicle.remove(); + return; + } + SeatEntity seatEntity = furniture.seatByPlayerId(player.getEntityId()); + if (seatEntity != null && !seatEntity.destroyed()) { + seatEntity.destroy(); + } + return; + } + + BukkitSeatEntity seatEntity = (BukkitSeatEntity) serverPlayer.seat(); + if (seatEntity == null || seatEntity.literalObject() != vehicle) return; + + BukkitFurniture furniture = seatEntity.furniture(); if (furniture == null) { + seatEntity.destroy(); return; } - String vector3f = vehicle.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING); - if (vector3f == null) { - plugin.logger().warn("Failed to get vector3f for player " + player.getName() + "'s seat"); - return; - } - Vector3f seatPos = MiscUtils.getAsVector3f(vector3f, "seat"); - furniture.removeOccupiedSeat(seatPos); + + seatEntity.dismount(serverPlayer); if (player.getVehicle() != null) return; Location vehicleLocation = vehicle.getLocation(); diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/FurnitureEventListener.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/FurnitureEventListener.java index f9c526233..b45c2f251 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/FurnitureEventListener.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/FurnitureEventListener.java @@ -2,6 +2,8 @@ package net.momirealms.craftengine.bukkit.entity.furniture; import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent; import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent; +import net.momirealms.craftengine.bukkit.api.BukkitAdaptors; +import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; import org.bukkit.entity.ItemDisplay; @@ -10,7 +12,9 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.event.player.PlayerAnimationEvent; +import org.bukkit.event.player.PlayerArmorStandManipulateEvent; +import org.bukkit.event.player.PlayerItemHeldEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.world.EntitiesLoadEvent; @@ -123,12 +127,30 @@ public class FurnitureEventListener implements Listener { // do not allow players to put item on seats @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onInteractArmorStand(PlayerInteractAtEntityEvent event) { - Entity clicked = event.getRightClicked(); - if (clicked instanceof ArmorStand armorStand) { - Integer baseFurniture = armorStand.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER); - if (baseFurniture == null) return; - event.setCancelled(true); - } + public void onInteractArmorStand(PlayerArmorStandManipulateEvent event) { + ArmorStand clicked = event.getRightClicked(); + Integer baseFurniture = clicked.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER); + if (baseFurniture == null) return; + event.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) + public void onMainHandChange(PlayerItemHeldEvent event) { + Player player = event.getPlayer(); + if (player.getVehicle() == null) return; + BukkitServerPlayer serverPlayer = BukkitAdaptors.adapt(player); + if (serverPlayer.seat() == null) return; + + serverPlayer.seat().event(serverPlayer, event); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerAnimation(PlayerAnimationEvent event) { + Player player = event.getPlayer(); + if (player.getVehicle() == null) return; + BukkitServerPlayer serverPlayer = BukkitAdaptors.adapt(player); + if (serverPlayer.seat() == null) return; + + serverPlayer.seat().event(serverPlayer, event); } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/BukkitSeatEntity.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/BukkitSeatEntity.java new file mode 100644 index 000000000..06567d660 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/BukkitSeatEntity.java @@ -0,0 +1,81 @@ +package net.momirealms.craftengine.bukkit.entity.furniture.seat; + +import net.momirealms.craftengine.bukkit.api.BukkitAdaptors; +import net.momirealms.craftengine.bukkit.entity.BukkitEntity; +import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture; +import net.momirealms.craftengine.core.entity.furniture.Furniture; +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; +import net.momirealms.craftengine.core.plugin.network.NetWorkUser; +import org.bukkit.entity.Entity; +import org.joml.Vector3f; + +public abstract class BukkitSeatEntity extends BukkitEntity implements SeatEntity { + private final BukkitFurniture furniture; + private final Vector3f vector3f; + private final int playerID; + private boolean destroyed = false; + + public BukkitSeatEntity(Entity entity, Furniture furniture, Vector3f vector3f, int playerID) { + super(entity); + this.furniture = (BukkitFurniture) furniture; + this.vector3f = vector3f; + this.playerID = playerID; + } + + @Override + public void add(NetWorkUser to) {} + + @Override + public void dismount(Player player) { + if (player == null) return; + player.setSeat(null); + onDismount(player); + destroy(); + } + + @Override + public void onDismount(Player player) {} + + @Override + public void event(Player player, Object event) {} + + @Override + public void destroy() { + if (destroyed) return; + destroyed = true; + + org.bukkit.entity.Entity entity = this.literalObject(); + if (entity == null) return; + + for (org.bukkit.entity.Entity passenger : entity.getPassengers()) { + entity.removePassenger(passenger); + if (passenger instanceof org.bukkit.entity.Player p && p.getEntityId() == this.playerID) { + Player cePlayer = BukkitAdaptors.adapt(p); + dismount(cePlayer); + } + } + entity.remove(); + furniture.removeSeatEntity(playerID()); + furniture.removeOccupiedSeat(vector3f()); + } + + public boolean destroyed() { + return destroyed; + } + + @Override + public BukkitFurniture furniture() { + return this.furniture; + } + + @Override + public Vector3f vector3f() { + return this.vector3f; + } + + @Override + public int playerID() { + return this.playerID; + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/BukkitSeatTypes.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/BukkitSeatTypes.java new file mode 100644 index 000000000..2e73c8707 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/BukkitSeatTypes.java @@ -0,0 +1,12 @@ +package net.momirealms.craftengine.bukkit.entity.furniture.seat; + +import net.momirealms.craftengine.core.entity.furniture.SeatType; + +public class BukkitSeatTypes extends SeatType { + + public static void init() { + register(SIT, SitSeat.FACTORY); + register(LAY, LaySeat.FACTORY); + register(CRAWL, CrawlSeat.FACTORY); + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/CrawlSeat.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/CrawlSeat.java new file mode 100644 index 000000000..ee34c2e9f --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/CrawlSeat.java @@ -0,0 +1,157 @@ +package net.momirealms.craftengine.bukkit.entity.furniture.seat; + +import net.momirealms.craftengine.bukkit.api.BukkitAdaptors; +import net.momirealms.craftengine.bukkit.entity.data.PlayerData; +import net.momirealms.craftengine.bukkit.entity.data.ShulkerData; +import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture; +import net.momirealms.craftengine.bukkit.nms.FastNMS; +import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; +import net.momirealms.craftengine.bukkit.plugin.network.BukkitNetworkManager; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.MAttributeHolders; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.MEntityTypes; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.NetworkReflections; +import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer; +import net.momirealms.craftengine.bukkit.util.PlayerUtils; +import net.momirealms.craftengine.core.entity.furniture.*; +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.plugin.network.NetWorkUser; +import net.momirealms.craftengine.core.util.Key; +import net.momirealms.craftengine.core.util.MiscUtils; +import net.momirealms.craftengine.core.util.VersionHelper; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Pose; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public class CrawlSeat extends AbstractSeat { + public static final SeatFactory FACTORY = new Factory(); + private static final List visualData = new ArrayList<>(); + private final boolean limitPlayerRotation; + + static { + ShulkerData.NoGravity.addEntityDataIfNotDefaultValue(true, visualData); + ShulkerData.Silent.addEntityDataIfNotDefaultValue(true, visualData); + ShulkerData.MobFlags.addEntityDataIfNotDefaultValue((byte) 0x01, visualData); + ShulkerData.SharedFlags.addEntityDataIfNotDefaultValue((byte) 0x20, visualData); + } + + public CrawlSeat(Vector3f offset, float yaw, boolean limitPlayerRotation) { + super(offset, yaw); + this.limitPlayerRotation = limitPlayerRotation; + } + + @Override + public SeatEntity spawn(Player player, Furniture furniture) { + return spawn((org.bukkit.entity.Player) player.platformPlayer(), furniture); + } + + public SeatEntity spawn(org.bukkit.entity.Player player, Furniture furniture) { + Location location = ((BukkitFurniture) furniture).calculateSeatLocation(this); + + org.bukkit.entity.Entity seatEntity = BukkitFurniture.spawnSeatEntity(furniture, player.getWorld(), location, this.limitPlayerRotation, null); + if (!seatEntity.addPassenger(player)) { + seatEntity.remove(); + return null; + } + + // Fix Rider Pose + int entityId = CoreReflections.instance$Entity$ENTITY_COUNTER.incrementAndGet(); + List packets = new ArrayList<>(); + packets.add(FastNMS.INSTANCE.constructor$ClientboundAddEntityPacket(entityId, UUID.randomUUID(), location.getX(), location.getY(), location.getZ(), location.getPitch(), location.getYaw(), + MEntityTypes.SHULKER, 0, CoreReflections.instance$Vec3$Zero, 0)); + packets.add(FastNMS.INSTANCE.constructor$ClientboundSetEntityDataPacket(entityId, List.copyOf(visualData))); + + try { + if (VersionHelper.isOrAbove1_20_5()) { + Object attributeInstance = CoreReflections.constructor$AttributeInstance.newInstance(MAttributeHolders.SCALE, (Consumer) (o) -> {}); + CoreReflections.method$AttributeInstance$setBaseValue.invoke(attributeInstance, 0.6); + packets.add( + NetworkReflections.constructor$ClientboundUpdateAttributesPacket0 + .newInstance(entityId, Collections.singletonList(attributeInstance)) + ); + packets.add(FastNMS.INSTANCE.constructor$ClientboundSetPassengersPacket(seatEntity.getEntityId(), entityId)); + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to add crawl seat attributes", e); + } + + Object bundle = FastNMS.INSTANCE.constructor$ClientboundBundlePacket(packets); + BukkitServerPlayer serverPlayer = BukkitAdaptors.adapt(player); + serverPlayer.sendPacket(bundle, true); + + // Sync Pose + player.setPose(Pose.SWIMMING, true); + Object syncPosePacket = null; + try { + Object playerData = CoreReflections.method$Entity$getEntityData.invoke(serverPlayer.serverPlayer()); + Object dataItem = CoreReflections.method$SynchedEntityData$getItem.invoke(playerData, PlayerData.Pose.entityDataAccessor()); + Object dataValue = CoreReflections.method$SynchedEntityData$DataItem$value.invoke(dataItem); + syncPosePacket = FastNMS.INSTANCE.constructor$ClientboundSetEntityDataPacket(serverPlayer.entityID(), List.of(dataValue)); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to construct sync pose packet", e); + } + + Object finalSyncPosePacket = syncPosePacket; + BukkitCraftEngine.instance().scheduler().sync().runLater(() -> { + serverPlayer.sendPacket(finalSyncPosePacket, true); + for (org.bukkit.entity.Player p : PlayerUtils.getTrackedBy(player)) { + BukkitNetworkManager.instance().sendPacket(BukkitAdaptors.adapt(p), finalSyncPosePacket, true); + } + }, 1, location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4); + + return new CrawlEntity(seatEntity, furniture, offset(), player.getEntityId(), entityId, syncPosePacket); + } + + private static class CrawlEntity extends BukkitSeatEntity { + private final int entityId; + private final Object syncPosePacket; + + + public CrawlEntity(Entity entity, Furniture furniture, Vector3f vector3f, int playerID, int entityId, Object fixPosePacket) { + super(entity, furniture, vector3f, playerID); + this.entityId = entityId; + this.syncPosePacket = fixPosePacket; + } + + @Override + public void add(NetWorkUser to) { + to.sendPacket(syncPosePacket, false); + } + + @Override + public void onDismount(Player player) { + if (player == null) return; + org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) player.platformPlayer(); + bukkitPlayer.setPose(Pose.STANDING, false); + try { + Object packet = NetworkReflections.constructor$ClientboundRemoveEntitiesPacket.newInstance((Object) new int[]{entityId}); + player.sendPacket(packet, false); + } catch (Exception e) { + BukkitCraftEngine.instance().logger().warn("Failed to dismount from CrawlEntity", e); + } + } + + @Override + public Key type() { + return SeatType.CRAWL; + } + } + + public static class Factory implements SeatFactory { + + @Override + public Seat create(List args) { + if (args.size() == 1) return new CrawlSeat(MiscUtils.getAsVector3f(args.getFirst(), "seats"), 0, false); + return new CrawlSeat(MiscUtils.getAsVector3f(args.getFirst(), "seats"), Float.parseFloat(args.get(1)), true); + } + } +} \ No newline at end of file diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/LaySeat.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/LaySeat.java new file mode 100644 index 000000000..b8cc1fb41 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/LaySeat.java @@ -0,0 +1,698 @@ +package net.momirealms.craftengine.bukkit.entity.furniture.seat; + +import com.google.common.collect.Multimap; +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.ints.IntList; +import net.momirealms.craftengine.bukkit.entity.data.LivingEntityData; +import net.momirealms.craftengine.bukkit.entity.data.PlayerData; +import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture; +import net.momirealms.craftengine.bukkit.nms.FastNMS; +import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; +import net.momirealms.craftengine.bukkit.plugin.network.BukkitNetworkManager; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.MEntityTypes; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.MItems; +import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.NetworkReflections; +import net.momirealms.craftengine.bukkit.plugin.scheduler.impl.FoliaTask; +import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer; +import net.momirealms.craftengine.bukkit.util.*; +import net.momirealms.craftengine.core.entity.EquipmentSlot; +import net.momirealms.craftengine.core.entity.furniture.*; +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.plugin.network.ByteBufPacketEvent; +import net.momirealms.craftengine.core.plugin.network.NMSPacketEvent; +import net.momirealms.craftengine.core.plugin.network.NetWorkUser; +import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask; +import net.momirealms.craftengine.core.util.*; +import net.momirealms.craftengine.core.world.BlockPos; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.block.data.BlockData; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.player.PlayerAnimationEvent; +import org.bukkit.event.player.PlayerAnimationType; +import org.bukkit.event.player.PlayerItemHeldEvent; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffectType; +import org.joml.Vector3f; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static net.momirealms.craftengine.core.plugin.network.ProtocolVersion.V1_21_2; + +public class LaySeat extends AbstractSeat { + public static final SeatFactory FACTORY = new Factory(); + private static final List> emptyEquipments; + private static final List> emptyBukkitEquipments; + private static Method method$InventoryView$convertSlot; + private static Method method$InventoryView$getTopInventory; + private static Method method$InventoryView$getType; + private final Direction facing; + private final boolean sleep; + private final boolean phantom; + + static { + if (!VersionHelper.isOrAbove1_21_1()) { + method$InventoryView$convertSlot = ReflectionUtils.getMethod(InventoryView.class, new String[]{"convertSlot"}, int.class); + method$InventoryView$getTopInventory = ReflectionUtils.getMethod(InventoryView.class, new String[]{"getTopInventory"}); + method$InventoryView$getType = ReflectionUtils.getMethod(Inventory.class, new String[]{"getType"}); + } + emptyEquipments = List.of( + Pair.of(CoreReflections.instance$EquipmentSlot$MAINHAND, MItems.AIR$Item), + Pair.of(CoreReflections.instance$EquipmentSlot$OFFHAND, MItems.AIR$Item), + Pair.of(CoreReflections.instance$EquipmentSlot$HEAD, MItems.AIR$Item), + Pair.of(CoreReflections.instance$EquipmentSlot$CHEST, MItems.AIR$Item), + Pair.of(CoreReflections.instance$EquipmentSlot$LEGS, MItems.AIR$Item), + Pair.of(CoreReflections.instance$EquipmentSlot$FEET, MItems.AIR$Item) + ); + emptyBukkitEquipments = List.of( + Pair.of(CoreReflections.instance$EquipmentSlot$MAINHAND, ItemUtils.AIR), + Pair.of(CoreReflections.instance$EquipmentSlot$OFFHAND, ItemUtils.AIR), + Pair.of(CoreReflections.instance$EquipmentSlot$HEAD, ItemUtils.AIR), + Pair.of(CoreReflections.instance$EquipmentSlot$CHEST, ItemUtils.AIR), + Pair.of(CoreReflections.instance$EquipmentSlot$LEGS, ItemUtils.AIR), + Pair.of(CoreReflections.instance$EquipmentSlot$FEET, ItemUtils.AIR) + ); + } + + public LaySeat(Vector3f offset, Direction facing, boolean sleep, boolean phantom) { + super(offset, 0); + this.facing = facing; + this.sleep = sleep; + this.phantom = phantom; + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public SeatEntity spawn(Player cePlayer, Furniture furniture) { + Location loc = ((BukkitFurniture) furniture).calculateSeatLocation(this); + + org.bukkit.entity.Player player = (org.bukkit.entity.Player) cePlayer.platformPlayer(); + Object serverPlayer = cePlayer.serverPlayer(); + + // Pose offset nearly same as vanilla + AttributeInstance attribute = VersionHelper.isOrAbove1_21_2() ? player.getAttribute(Attribute.SCALE) : null; + double scale = attribute == null ? 1 : attribute.getValue(); + loc.add(0, 0.08525 * scale, 0); + + try { + List packets = new ArrayList<>(); + // NPC + Object server = CoreReflections.method$MinecraftServer$getServer.invoke(null); + Object level = FastNMS.INSTANCE.field$CraftWorld$ServerLevel(player.getWorld()); + UUID uuid = UUID.randomUUID(); + Object npcProfile = CoreReflections.constructor$GameProfile.newInstance(uuid, player.getName()); + Object playerProfile = CoreReflections.method$ServerPlayer$getGameProfile.invoke(serverPlayer); + + Multimap properties = (Multimap) CoreReflections.method$GameProfile$getProperties.invoke(npcProfile); + properties.putAll((Multimap) CoreReflections.method$GameProfile$getProperties.invoke(playerProfile)); + + Object npc; + if (VersionHelper.isOrAbove1_20_2()) { + Object clientInfo = CoreReflections.method$ServerPlayer$clientInformation.invoke(serverPlayer); + npc = CoreReflections.constructor$ServerPlayer.newInstance(server, level, npcProfile, clientInfo); + } else { + npc = CoreReflections.constructor$ServerPlayer.newInstance(server, level, npcProfile); + } + int npcId = FastNMS.INSTANCE.method$Entity$getId(npc); + CoreReflections.method$Entity$absSnapTo.invoke(npc, loc.getX(), loc.getY(), loc.getZ(), 0, 0); + Object npcSpawnPacket; + if (!VersionHelper.isOrAbove1_20_2()) { + npcSpawnPacket = NetworkReflections.constructor$ClientboundAddPlayerPacket.newInstance(npc); + } else { + npcSpawnPacket = FastNMS.INSTANCE.constructor$ClientboundAddEntityPacket(npcId, uuid, + loc.getX(), loc.getY(), loc.getZ(), 0, 0, + MEntityTypes.PLAYER, 0, CoreReflections.instance$Vec3$Zero, 0); + } + + // Info + EnumSet enumSet = EnumSet.noneOf((Class) NetworkReflections.clazz$ClientboundPlayerInfoUpdatePacket$Action); + enumSet.add(NetworkReflections.instance$ClientboundPlayerInfoUpdatePacket$Action$ADD_PLAYER); + Object entry; + if (VersionHelper.isOrAbove1_21_4()) { + entry = NetworkReflections.constructor$ClientBoundPlayerInfoUpdatePacket$Entry.newInstance( + uuid, npcProfile, false, 0, CoreReflections.instance$GameType$SURVIVAL, null, true, 0, null); + } else if (VersionHelper.isOrAbove1_21_3()) { + entry = NetworkReflections.constructor$ClientBoundPlayerInfoUpdatePacket$Entry.newInstance( + uuid, npcProfile, false, 0, CoreReflections.instance$GameType$SURVIVAL, null, 0, null); + } else { + entry = NetworkReflections.constructor$ClientBoundPlayerInfoUpdatePacket$Entry.newInstance( + uuid, npcProfile, false, 0, CoreReflections.instance$GameType$SURVIVAL, null, null); + } + Object npcInfoPacket = FastNMS.INSTANCE.constructor$ClientboundPlayerInfoUpdatePacket(enumSet, Collections.singletonList(entry)); + + // Bed + Direction bedDir = Direction.fromYaw(loc.getYaw() + Direction.getYaw(facing)); + if (bedDir == Direction.EAST || bedDir == Direction.WEST) bedDir = bedDir.opposite(); + BlockData bedData = Material.WHITE_BED.createBlockData("[facing=" + bedDir.name().toLowerCase() + ",part=head]"); + Location bedLoc = loc.clone(); + bedLoc.setY(bedLoc.getWorld().getMinHeight()); + Object bedPos = LocationUtils.toBlockPos(new BlockPos(bedLoc.getBlockX(), bedLoc.getBlockY(), bedLoc.getBlockZ())); + Object blockState = BlockStateUtils.blockDataToBlockState(bedData); + Object bedPacket = NetworkReflections.constructor$ClientboundBlockUpdatePacket.newInstance(bedPos, blockState); + + // Data + Object npcData = CoreReflections.method$Entity$getEntityData.invoke(npc); + Object playerData = CoreReflections.method$Entity$getEntityData.invoke(serverPlayer); + CoreReflections.method$Entity$setInvisible.invoke(serverPlayer, true); + CoreReflections.method$SynchedEntityData$set.invoke(npcData, PlayerData.Pose.entityDataAccessor(), CoreReflections.instance$Pose$SLEEPING); + CoreReflections.method$SynchedEntityData$set.invoke(npcData, LivingEntityData.SleepingPos.entityDataAccessor(), Optional.of(bedPos)); + CoreReflections.method$SynchedEntityData$set.invoke(npcData, PlayerData.PlayerModeCustomisation.entityDataAccessor(), CoreReflections.method$SynchedEntityData$get.invoke(playerData, PlayerData.PlayerModeCustomisation.entityDataAccessor())); + CoreReflections.method$SynchedEntityData$set.invoke(npcData, PlayerData.PlayerMainHand.entityDataAccessor(), CoreReflections.method$SynchedEntityData$get.invoke(playerData, PlayerData.PlayerMainHand.entityDataAccessor())); + CoreReflections.method$SynchedEntityData$set.invoke(npcData, PlayerData.ShoulderLeft.entityDataAccessor(), CoreReflections.method$SynchedEntityData$get.invoke(playerData, PlayerData.ShoulderLeft.entityDataAccessor())); + CoreReflections.method$SynchedEntityData$set.invoke(npcData, PlayerData.ShoulderRight.entityDataAccessor(), CoreReflections.method$SynchedEntityData$get.invoke(playerData, PlayerData.ShoulderRight.entityDataAccessor())); + CoreReflections.method$SynchedEntityData$set.invoke(playerData, PlayerData.ShoulderLeft.entityDataAccessor(), CoreReflections.instance$CompoundTag$Empty); + CoreReflections.method$SynchedEntityData$set.invoke(playerData, PlayerData.ShoulderRight.entityDataAccessor(), CoreReflections.instance$CompoundTag$Empty); + + // SetData + CoreReflections.method$Entity$setInvisible.invoke(serverPlayer, true); + Object npcDataPacket = FastNMS.INSTANCE.constructor$ClientboundSetEntityDataPacket( + npcId, (List) CoreReflections.method$SynchedEntityData$packDirty.invoke(npcData) + ); + + // Remove + Object npcRemovePacket = NetworkReflections.constructor$ClientboundRemoveEntitiesPacket.newInstance((Object) new int[]{npcId}); + + // TP + Object npcTeleportPacket; + if (VersionHelper.isOrAbove1_21_3()) { + Object positionMoveRotation = CoreReflections.method$PositionMoveRotation$of.invoke(null, npc); + npcTeleportPacket = NetworkReflections.constructor$ClientboundTeleportEntityPacket.newInstance(npcId, positionMoveRotation, Set.of(), false); + } else { + npcTeleportPacket = NetworkReflections.constructor$ClientboundTeleportEntityPacket.newInstance(npc); + } + + // Equipment + Object emptyEquipPacket = NetworkReflections.constructor$ClientboundSetEquipmentPacket.newInstance(player.getEntityId(), emptyEquipments); + + Map equipments = new HashMap<>(); + EntityEquipment equipment = player.getEquipment(); + for (org.bukkit.inventory.EquipmentSlot slot : org.bukkit.inventory.EquipmentSlot.values()) { + if ((!slot.isHand() && !slot.isArmor()) + || (VersionHelper.isOrAbove1_20_5() && slot == org.bukkit.inventory.EquipmentSlot.BODY)) { + continue; + } + EquipmentSlot slotId = EntityUtils.toCEEquipmentSlot(slot); + ItemStack item = equipment.getItem(slot); + equipments.put(slotId, item); + } + List> npcSlots = new ArrayList<>(); + equipments.forEach((slot, item) -> npcSlots.add(Pair.of(EntityUtils.fromEquipmentSlot(slot), FastNMS.INSTANCE.method$CraftItemStack$asNMSCopy(item)))); + Object fullEquipPacket = NetworkReflections.constructor$ClientboundSetEquipmentPacket.newInstance(npcId, npcSlots); + + // Animation + Object npcLeftAnimatePacket = NetworkReflections.constructor$ClientboundAnimatePacket.newInstance(npc, 0); + Object npcRightAnimatePacket = NetworkReflections.constructor$ClientboundAnimatePacket.newInstance(npc, 3); + + packets.add(npcInfoPacket); + packets.add(npcSpawnPacket); + packets.add(bedPacket); + packets.add(npcDataPacket); + packets.add(npcTeleportPacket); + packets.add(emptyEquipPacket); + packets.add(npcLeftAnimatePacket); + Object npcInitPackets = FastNMS.INSTANCE.constructor$ClientboundBundlePacket(packets); + + // Spawn + org.bukkit.entity.Entity seatEntity = BukkitFurniture.spawnSeatEntity(furniture, player.getWorld(), loc, false, null); + if (!seatEntity.addPassenger(player)) { // 0.5 higher + seatEntity.remove(); + return null; + } + cePlayer.sendPacket(npcInitPackets, true); + cePlayer.sendPacket(fullEquipPacket, true); + if (player.getY() > 0 && cePlayer.protocolVersion().isVersionNewerThan(V1_21_2)) { + BukkitCraftEngine.instance().scheduler().asyncLater(() -> cePlayer.sendPacket(npcTeleportPacket, true), + 50, TimeUnit.MILLISECONDS); // over height 0 cost 2 npcTeleportPacket + } + + Set trackers = new HashSet<>(); + for (org.bukkit.entity.Player o : PlayerUtils.getTrackedBy(player)) { + NetWorkUser tracker = BukkitNetworkManager.instance().getOnlineUser(o); + tracker.sendPacket(npcInitPackets, false); + tracker.sendPacket(fullEquipPacket, false); + if (player.getY() > 0 && tracker.protocolVersion().isVersionNewerThan(V1_21_2)) { + BukkitCraftEngine.instance().scheduler().asyncLater(() -> tracker.sendPacket(npcTeleportPacket, false), + 50, TimeUnit.MILLISECONDS); + } + trackers.add(tracker); + } + + // HeadRot + Direction npcDir = bedDir.opposite(); + + if (sleep) { + player.setSleepingIgnored(true); + } + + if (phantom) { + player.setStatistic(Statistic.TIME_SINCE_REST, 0); + } + + return new LayEntity( + seatEntity, + furniture, + this.offset(), + npcInitPackets, + npcRemovePacket, + npcTeleportPacket, + npcLeftAnimatePacket, + npcRightAnimatePacket, + (BukkitServerPlayer) cePlayer, + trackers, + bedLoc, + npc, + npcId, + npcDir, + equipments, + emptyEquipPacket, + fullEquipPacket, + sleep + ); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to spawn LaySeat", e); + } + return null; + } + + private static class LayEntity extends BukkitSeatEntity { + private final Object npcInitPackets; + private final Object npcRemovePacket; + private final Object npcTPPacket; + private final Object npcLeftAnimatePacket; + private final Object npcRightAnimatePacket; + private final BukkitServerPlayer serverPlayer; + private final Object npc; + private final Location bedLoc; + private final int npcID; + private final Direction npcDir; + private final Set trackers; + + // Equipment + private final PlayerMonitorTask task; + private final Map equipments; + private final Object emptyEquipPacket; + private Object updateEquipPacket; + private Object fullEquipPacket; + + private final boolean sleep; + private Object npcRotHeadPacket; + private Object npcDataPacket; + + public LayEntity( + org.bukkit.entity.Entity entity, + Furniture furniture, + Vector3f vector, + Object npcInitPackets, + Object npcRemovePacket, + Object npcTPPacket, + Object npcLeftAnimatePacket, + Object npcRightAnimatePacket, + BukkitServerPlayer serverPlayer, + Set trackers, + Location bedLoc, + Object npc, + int npcID, + Direction npcDir, + Map equipments, + Object emptyEquipPacket, + Object fullEquipPacket, + boolean sleep + ) { + super(entity, furniture, vector, serverPlayer.entityID()); + this.npcInitPackets = npcInitPackets; + this.npcRemovePacket = npcRemovePacket; + this.npcTPPacket = npcTPPacket; + this.npcLeftAnimatePacket = npcLeftAnimatePacket; + this.npcRightAnimatePacket = npcRightAnimatePacket; + this.serverPlayer = serverPlayer; + this.trackers = trackers; + this.bedLoc = bedLoc; + this.npc = npc; + this.npcID = npcID; + this.npcDir = npcDir; + + this.task = new PlayerMonitorTask(); + this.equipments = equipments; + this.emptyEquipPacket = emptyEquipPacket; + this.fullEquipPacket = fullEquipPacket; + + this.sleep = sleep; + } + + @Override + public void add(NetWorkUser to) { + to.sendPacket(this.npcInitPackets, false); + to.sendPacket(this.fullEquipPacket, false); + to.sendPacket(this.npcRotHeadPacket, false); + if (npcDataPacket != null) to.sendPacket(this.npcDataPacket, false); + if (serverPlayer.y() > 0 && to.protocolVersion().isVersionNewerThan(V1_21_2)) { + BukkitCraftEngine.instance().scheduler().asyncLater(() -> + to.sendPacket(this.npcTPPacket, false), 50, TimeUnit.MILLISECONDS); + } + trackers.add(to); + } + + @Override + public boolean handleEntitiesRemove(NetWorkUser user, IntList entityIds) { + entityIds.add(npcID); + trackers.remove(user); + return true; + } + + @Override + public void dismount(Player player) { + super.dismount(player); + if (player != null) return; + try { // for disconnect recover + Object blockPos = LocationUtils.toBlockPos(bedLoc.getBlockX(), bedLoc.getBlockY(), bedLoc.getBlockZ()); + Object blockState = BlockStateUtils.blockDataToBlockState(bedLoc.getBlock().getBlockData()); + Object blockRecoverPacket = NetworkReflections.constructor$ClientboundBlockUpdatePacket.newInstance(blockPos, blockState); + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(blockRecoverPacket, false); + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to dismount player", e); + } + } + + @Override + public void onDismount(Player player) { + this.task.task.cancel(); + + org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) player.platformPlayer(); + Object blockPos = LocationUtils.toBlockPos(bedLoc.getBlockX(), bedLoc.getBlockY(), bedLoc.getBlockZ()); + Object blockState = BlockStateUtils.blockDataToBlockState(bedLoc.getBlock().getBlockData()); + + try { + Object blockRecoverPacket = NetworkReflections.constructor$ClientboundBlockUpdatePacket.newInstance(blockPos, blockState); + player.sendPacket(this.npcRemovePacket, true); + player.sendPacket(blockRecoverPacket, true); + + if (bukkitPlayer.getPotionEffect(PotionEffectType.INVISIBILITY) == null) { + CoreReflections.method$Entity$setInvisible.invoke(serverPlayer.serverPlayer(), false); + } + + Object npcData = CoreReflections.method$Entity$getEntityData.invoke(npc); + Object playerData = CoreReflections.method$Entity$getEntityData.invoke(serverPlayer.serverPlayer()); + CoreReflections.method$SynchedEntityData$set.invoke( + playerData, + PlayerData.ShoulderLeft.entityDataAccessor(), + CoreReflections.method$SynchedEntityData$get.invoke(npcData, PlayerData.ShoulderLeft.entityDataAccessor()) + ); + CoreReflections.method$SynchedEntityData$set.invoke( + playerData, + PlayerData.ShoulderRight.entityDataAccessor(), + CoreReflections.method$SynchedEntityData$get.invoke(npcData, PlayerData.ShoulderRight.entityDataAccessor()) + ); + + bukkitPlayer.updateInventory(); + + if (sleep) { + bukkitPlayer.setSleepingIgnored(false); + } + + Object fullSlots = NetworkReflections.method$ClientboundSetEquipmentPacket$getSlots.invoke(this.fullEquipPacket); + Object recoverEquip = NetworkReflections.constructor$ClientboundSetEquipmentPacket.newInstance(bukkitPlayer.getEntityId(), fullSlots); + + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(this.npcRemovePacket, false); + tracker.sendPacket(blockRecoverPacket, false); + tracker.sendPacket(recoverEquip, false); + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to dismount from LayEntity", e); + } + } + + public void equipmentChange(Map equipmentChanges, int previousSlot) { + org.bukkit.entity.Player player = serverPlayer.platformPlayer(); + List> changedSlots = new ArrayList<>(); + + for (Map.Entry entry : equipmentChanges.entrySet()) { + Object slotId = EntityUtils.fromEquipmentSlot(entry.getKey()); + Object itemStack = FastNMS.INSTANCE.method$CraftItemStack$asNMSCopy(entry.getValue()); + changedSlots.add(Pair.of(slotId, itemStack)); + } + this.equipments.putAll(equipmentChanges); + + List> allSlots = new ArrayList<>(); + equipments.forEach((slot, item) -> + allSlots.add(Pair.of(EntityUtils.fromEquipmentSlot(slot), FastNMS.INSTANCE.method$CraftItemStack$asNMSCopy(item)))); + try { + this.updateEquipPacket = NetworkReflections.constructor$ClientboundSetEquipmentPacket.newInstance(npcID, changedSlots); + this.fullEquipPacket = NetworkReflections.constructor$ClientboundSetEquipmentPacket.newInstance(npcID, allSlots); + if (previousSlot != -1) { + player.updateInventory(); + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to handle equipmentChange", e); + } + + serverPlayer.sendPacket(this.emptyEquipPacket, false); + serverPlayer.sendPacket(this.updateEquipPacket, false); + + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(this.updateEquipPacket, false); + } + } + + @Override + public void handleSetEquipment(NetWorkUser user, ByteBufPacketEvent event, Object slots) { + if (emptyBukkitEquipments.equals(slots)) return; + event.setCancelled(true); + } + + @Override + public void handleContainerSetSlot(NetWorkUser user, NMSPacketEvent event, Object packet) { + try { + int slot = (int) NetworkReflections.method$ClientboundContainerSetSlotPacket$getSlot.invoke(packet); + org.bukkit.entity.Player player = (org.bukkit.entity.Player) user.platformPlayer(); + + int convertSlot; + boolean isPlayerInv; + + if (!VersionHelper.isOrAbove1_21_1()) { + Object openInventory = player.getOpenInventory(); + convertSlot = (int) method$InventoryView$convertSlot.invoke(openInventory, slot); + Object topInventory = method$InventoryView$getTopInventory.invoke(openInventory); + Object type = method$InventoryView$getType.invoke(topInventory); + isPlayerInv = type == InventoryType.CRAFTING; + } else { + convertSlot = player.getOpenInventory().convertSlot(slot); + isPlayerInv = player.getOpenInventory().getTopInventory().getType() == InventoryType.CRAFTING; + } + + if (!(convertSlot == player.getInventory().getHeldItemSlot() || (isPlayerInv && (slot == 45 || (slot >= 5 && slot <= 8))))) + return; + int containerId = (int) NetworkReflections.method$ClientboundContainerSetSlotPacket$getContainerId.invoke(packet); + int stateId = (int) NetworkReflections.method$ClientboundContainerSetSlotPacket$getStateId.invoke(packet); + Object replacePacket = NetworkReflections.constructor$ClientboundContainerSetSlotPacket.newInstance(containerId, stateId, slot, MItems.AIR$Item); + event.replacePacket(replacePacket); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to handleContainerSetSlotPacket", e); + } + } + + @Override + public void event(Player player, Object event) { + if (event instanceof PlayerAnimationEvent e) { + try { + Object animatePacket; + if (e.getAnimationType() == PlayerAnimationType.ARM_SWING) { + animatePacket = npcLeftAnimatePacket; + } else { + animatePacket = npcRightAnimatePacket; + } + serverPlayer.sendPacket(animatePacket, true); + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(animatePacket, true); + } + } catch (Exception exception) { + CraftEngine.instance().logger().warn("Failed to handle PlayerAnimationEvent", exception); + } + } else if (event instanceof PlayerItemHeldEvent e) { + ItemStack item = e.getPlayer().getInventory().getItem(e.getNewSlot()); + if (item == null) item = ItemUtils.AIR; + + equipmentChange(Map.of(EquipmentSlot.MAIN_HAND, item), e.getPreviousSlot()); + } + } + + @Override + public Key type() { + return SeatType.LAY; + } + + private class PlayerMonitorTask implements Runnable { + + private final SchedulerTask task; + private float lastYaw; + + private PlayerMonitorTask() { + org.bukkit.entity.Player p = serverPlayer.platformPlayer(); + BukkitCraftEngine plugin = BukkitCraftEngine.instance(); + if (VersionHelper.isFolia()) { + this.task = new FoliaTask(p.getScheduler().runAtFixedRate(plugin.javaPlugin(), (t) -> this.run(), () -> { + }, 1, 1)); + } else { + this.task = plugin.scheduler().sync().runRepeating(this, 0, 1); + } + } + + @Override + public void run() { + org.bukkit.entity.Player player = serverPlayer.platformPlayer(); + if (player == null || !player.isValid()) { + this.task.cancel(); + return; + } + + // Invisible + updateNpcInvisible(); + try { + if (!player.isInvisible()) + CoreReflections.method$Entity$setInvisible.invoke(serverPlayer.serverPlayer(), true); + } catch (Exception exception) { + CraftEngine.instance().logger().warn("Failed to set shared flag", exception); + } + + // Sync Rotation + float playerYaw = player.getYaw(); + if (lastYaw != playerYaw) { + updateNpcYaw(playerYaw); + serverPlayer.sendPacket(npcRotHeadPacket, false); + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(npcRotHeadPacket, true); + } + this.lastYaw = playerYaw; + } + + // Sync Equipment + Map newEquipments = new HashMap<>(); + for (EquipmentSlot slot : EquipmentSlot.values()) { + if (!slot.isHand() && !slot.isPlayerArmor()) continue; + ItemStack newItem = player.getEquipment().getItem(EntityUtils.toBukkitEquipmentSlot(slot)); + try { + ItemStack item = equipments.get(slot); + boolean isChange = !newItem.equals(item); + if (isChange) { + newEquipments.put(slot, newItem); + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to monitor equipments change", e); + } + } + + if (!newEquipments.isEmpty()) { + equipmentChange(newEquipments, -1); + return; + } + serverPlayer.sendPacket(emptyEquipPacket, false); + } + } + + private void updateNpcYaw(float playerYaw) { + byte packYaw = getRot(playerYaw); + try { + this.npcRotHeadPacket = NetworkReflections.constructor$ClientboundRotateHeadPacket.newInstance(npc, packYaw); + } catch (Exception exception) { + CraftEngine.instance().logger().warn("Failed to sync NPC yaw", exception); + } + } + + private byte getRot(float playerYaw) { + float npcYaw = Direction.getYaw(npcDir); + float centerYaw = normalizeYaw(npcYaw); + float playerYawNorm = normalizeYaw(playerYaw); + + float deltaYaw = normalizeYaw(playerYawNorm - centerYaw); + boolean isBehind = Math.abs(deltaYaw) > 90; + + float mappedYaw; + if (isBehind) { + float rel = Math.abs(deltaYaw) - 180; + mappedYaw = rel * (deltaYaw > 0 ? -1 : 1); + } else { + mappedYaw = deltaYaw; + } + + float finalYaw = Math.max(-45, Math.min(45, mappedYaw)); + return MCUtils.packDegrees(finalYaw); + } + + private float normalizeYaw(float yaw) { + yaw %= 360.0f; + if (yaw < -180.0f) yaw += 360.0f; + if (yaw >= 180.0f) yaw -= 360.0f; + return yaw; + } + + private void updateNpcInvisible() { + try { + org.bukkit.entity.Player player = serverPlayer.platformPlayer(); + if (player.getPotionEffect(PotionEffectType.INVISIBILITY) == null && npcDataPacket != null) { + npcDataPacket = null; + CoreReflections.method$Entity$setInvisible.invoke(npc, false); + Object npcData = CoreReflections.method$Entity$getEntityData.invoke(npc); + Object dataItem = CoreReflections.method$SynchedEntityData$getItem.invoke(npcData, PlayerData.SharedFlags.entityDataAccessor()); + Object dataValue = CoreReflections.method$SynchedEntityData$DataItem$value.invoke(dataItem); + Object packet = FastNMS.INSTANCE.constructor$ClientboundSetEntityDataPacket(npcID, List.of(dataValue)); + serverPlayer.sendPacket(packet, false); + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(packet, false); + } + } else if (player.getPotionEffect(PotionEffectType.INVISIBILITY) != null && npcDataPacket == null) { + CoreReflections.method$Entity$setInvisible.invoke(npc, true); + Object npcData = CoreReflections.method$Entity$getEntityData.invoke(npc); + Object dataItem = CoreReflections.method$SynchedEntityData$getItem.invoke(npcData, PlayerData.SharedFlags.entityDataAccessor()); + Object dataValue = CoreReflections.method$SynchedEntityData$DataItem$value.invoke(dataItem); + npcDataPacket = FastNMS.INSTANCE.constructor$ClientboundSetEntityDataPacket(npcID, List.of(dataValue)); + serverPlayer.sendPacket(npcDataPacket, false); + for (NetWorkUser tracker : trackers) { + tracker.sendPacket(npcDataPacket, false); + } + } + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to updateNpcInvisible", e); + } + } + } + + public static class Factory implements SeatFactory { + + @Override + public Seat create(List args) { + Vector3f offset = MiscUtils.getAsVector3f(args.get(0), "seats"); + Direction facing = args.size() > 1 ? parseFacing(args.get(1)) : Direction.SOUTH; + boolean sleep = args.size() > 2 && Boolean.parseBoolean(args.get(2)); + boolean phantom = args.size() > 4 && Boolean.parseBoolean(args.get(3)); + + if (facing == Direction.NORTH || facing == Direction.SOUTH) { + float temp = offset.x; + offset.x = offset.z; + offset.z = temp; + } + return new LaySeat(offset, facing, sleep, phantom); + } + + private Direction parseFacing(String facing) { + return switch (facing.toLowerCase()) { + case "back" -> Direction.NORTH; + case "left" -> Direction.WEST; + case "right" -> Direction.EAST; + default -> Direction.SOUTH; + }; + } + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/SitSeat.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/SitSeat.java new file mode 100644 index 000000000..a34f54391 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/entity/furniture/seat/SitSeat.java @@ -0,0 +1,59 @@ +package net.momirealms.craftengine.bukkit.entity.furniture.seat; + +import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture; +import net.momirealms.craftengine.core.entity.furniture.*; +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; +import net.momirealms.craftengine.core.util.Key; +import net.momirealms.craftengine.core.util.MiscUtils; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.joml.Vector3f; + +import java.util.List; + +public class SitSeat extends AbstractSeat { + public static final SeatFactory FACTORY = new Factory(); + private final boolean limitPlayerRotation; + + public SitSeat(Vector3f offset, float yaw, boolean limitPlayerRotation) { + super(offset, yaw); + this.limitPlayerRotation = limitPlayerRotation; + } + + @Override + public SeatEntity spawn(Player player, Furniture furniture) { + return spawn((org.bukkit.entity.Player) player.platformPlayer(), furniture); + } + + public SeatEntity spawn(org.bukkit.entity.Player player, Furniture furniture) { + Location location = ((BukkitFurniture) furniture).calculateSeatLocation(this); + org.bukkit.entity.Entity seatEntity = BukkitFurniture.spawnSeatEntity(furniture, player.getWorld(), location, this.limitPlayerRotation, null); + if (!seatEntity.addPassenger(player)) { + seatEntity.remove(); + return null; + } + return new SitEntity(seatEntity, furniture, offset(), player.getEntityId()); + } + + private static class SitEntity extends BukkitSeatEntity { + + public SitEntity(Entity entity, Furniture furniture, Vector3f vector3f, int playerID) { + super(entity, furniture, vector3f, playerID); + } + + @Override + public Key type() { + return SeatType.SIT; + } + } + + public static class Factory implements SeatFactory { + + @Override + public Seat create(List args) { + if (args.size() == 1) return new SitSeat(MiscUtils.getAsVector3f(args.getFirst(), "seats"), 0, false); + return new SitSeat(MiscUtils.getAsVector3f(args.getFirst(), "seats"), Float.parseFloat(args.get(1)), true); + } + } +} \ No newline at end of file diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java index e7017d8d9..6f6744670 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java @@ -8,6 +8,7 @@ import net.momirealms.craftengine.bukkit.block.behavior.BukkitBlockBehaviors; import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurnitureManager; import net.momirealms.craftengine.bukkit.entity.furniture.hitbox.BukkitHitBoxTypes; import net.momirealms.craftengine.bukkit.entity.projectile.BukkitProjectileManager; +import net.momirealms.craftengine.bukkit.entity.furniture.seat.BukkitSeatTypes; import net.momirealms.craftengine.bukkit.font.BukkitFontManager; import net.momirealms.craftengine.bukkit.item.BukkitItemManager; import net.momirealms.craftengine.bukkit.item.behavior.BukkitItemBehaviors; @@ -186,6 +187,7 @@ public class BukkitCraftEngine extends CraftEngine { BukkitItemBehaviors.init(); BukkitHitBoxTypes.init(); PacketConsumers.initEntities(RegistryUtils.currentEntityTypeRegistrySize()); + BukkitSeatTypes.init(); super.packManager = new BukkitPackManager(this); super.senderFactory = new BukkitSenderFactory(this); super.itemManager = new BukkitItemManager(this); diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java index 94bf052ad..c5e4f8fbb 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java @@ -162,6 +162,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes registerNMSPacketConsumer(PacketConsumers.MOVE_POS_ENTITY, NetworkReflections.clazz$ClientboundMoveEntityPacket$Pos); registerNMSPacketConsumer(PacketConsumers.ROTATE_HEAD, NetworkReflections.clazz$ClientboundRotateHeadPacket); registerNMSPacketConsumer(PacketConsumers.SET_ENTITY_MOTION, NetworkReflections.clazz$ClientboundSetEntityMotionPacket); + registerNMSPacketConsumer(PacketConsumers.SET_CONTAINER_SLOT, NetworkReflections.clazz$ClientboundContainerSetSlotPacket); registerNMSPacketConsumer(PacketConsumers.CLIENT_INFO, NetworkReflections.clazz$ServerboundClientInformationPacket); registerS2CByteBufPacketConsumer(PacketConsumers.LEVEL_CHUNK_WITH_LIGHT, this.packetIds.clientboundLevelChunkWithLightPacket()); registerS2CByteBufPacketConsumer(PacketConsumers.SECTION_BLOCK_UPDATE, this.packetIds.clientboundSectionBlocksUpdatePacket()); @@ -299,7 +300,11 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes } public NetWorkUser getOnlineUser(Player player) { - return this.onlineUsers.get(player.getUniqueId()); + return this.getOnlineUser(player.getUniqueId()); + } + + public NetWorkUser getOnlineUser(UUID uuid) { + return onlineUsers.get(uuid); } public Channel getChannel(Player player) { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java index aaf752116..d5d1b8b54 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java @@ -35,6 +35,8 @@ import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer; import net.momirealms.craftengine.bukkit.util.*; import net.momirealms.craftengine.core.block.ImmutableBlockState; import net.momirealms.craftengine.core.entity.player.InteractionHand; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; import net.momirealms.craftengine.core.font.FontManager; import net.momirealms.craftengine.core.font.IllegalCharacterProcessResult; import net.momirealms.craftengine.core.item.CustomItem; @@ -180,6 +182,17 @@ public class PacketConsumers { user.entityPacketHandlers().put(id, FurnitureCollisionPacketHandler.INSTANCE); } }; + ADD_ENTITY_HANDLERS[MEntityTypes.PLAYER$registryId] = (user, event) -> { + FriendlyByteBuf buf = event.getBuffer(); + buf.readVarInt(); + UUID uuid = buf.readUUID(); + BukkitServerPlayer player = (BukkitServerPlayer) BukkitCraftEngine.instance().networkManager().getOnlineUser(uuid); + if (player == null) return; + SeatEntity seat = player.seat(); + if (seat == null) return; + user.entityPacketHandlers().put(seat.playerID(), seat); + seat.add(user); + }; } private static BukkitNetworkManager.Handlers simpleAddEntityHandler(EntityPacketHandler handler) { @@ -1499,7 +1512,7 @@ public class PacketConsumers { for (int i = 0, size = intList.size(); i < size; i++) { int entityId = intList.getInt(i); EntityPacketHandler handler = user.entityPacketHandlers().remove(entityId); - if (handler != null && handler.handleEntitiesRemove(intList)) { + if (handler != null && handler.handleEntitiesRemove(user, intList)) { isChange = true; } } @@ -1649,6 +1662,8 @@ public class PacketConsumers { if (!serverPlayer.isSecondaryUseActive()) { furniture.findFirstAvailableSeat(entityId).ifPresent(seatPos -> { if (furniture.tryOccupySeat(seatPos)) { + SeatEntity currentSeat = serverPlayer.seat(); + if (currentSeat != null) currentSeat.dismount(serverPlayer); furniture.spawnSeatEntityForPlayer(serverPlayer, seatPos); } }); @@ -1915,6 +1930,10 @@ public class PacketConsumers { @SuppressWarnings("unchecked") public static final BiConsumer SET_ENTITY_DATA = (user, event) -> { try { + SeatEntity seat = ((BukkitServerPlayer)user).seat(); + if (seat != null) { + seat.handleSetEntityData(user, event); + } FriendlyByteBuf buf = event.getBuffer(); int id = buf.readVarInt(); EntityPacketHandler handler = user.entityPacketHandlers().get(id); @@ -2179,6 +2198,10 @@ public class PacketConsumers { FastNMS.INSTANCE.method$FriendlyByteBuf$writeItem(newFriendlyBuf, pair.getSecond()); } } + EntityPacketHandler handler = user.entityPacketHandlers().get(entity); + if (handler != null) { + handler.handleSetEquipment(user, event, slots); + } } catch (Exception e) { CraftEngine.instance().logger().warn("Failed to handle ClientboundSetEquipmentPacket", e); } @@ -2417,6 +2440,15 @@ public class PacketConsumers { } }; + public static final TriConsumer SET_CONTAINER_SLOT = (user, event, packet) -> { + try { + SeatEntity seat = ((BukkitServerPlayer) user).seat(); + if (seat != null) seat.handleContainerSetSlot(user, event, packet); + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to handle ClientboundSetEquipmentPacket", e); + } + }; + public static final TriConsumer CLIENT_INFO = (user, event, packet) -> { try { Map information = FastNMS.INSTANCE.method$ServerboundClientInformationPacket$information(packet); diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/handler/FurniturePacketHandler.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/handler/FurniturePacketHandler.java index 0a60cfc3a..8e26a3d3a 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/handler/FurniturePacketHandler.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/handler/FurniturePacketHandler.java @@ -15,7 +15,7 @@ public class FurniturePacketHandler implements EntityPacketHandler { } @Override - public boolean handleEntitiesRemove(IntList entityIds) { + public boolean handleEntitiesRemove(NetWorkUser user, IntList entityIds) { entityIds.addAll(this.fakeEntities); return true; } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/CoreReflections.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/CoreReflections.java index 85f4bf223..cd3076f48 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/CoreReflections.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/CoreReflections.java @@ -1,5 +1,6 @@ package net.momirealms.craftengine.bukkit.plugin.reflection.minecraft; +import com.google.common.collect.ForwardingMultimap; import com.google.common.collect.ImmutableList; import com.google.gson.JsonElement; import com.mojang.serialization.Codec; @@ -3504,6 +3505,155 @@ public final class CoreReflections { } } + public static final Method method$Entity$setInvisible = requireNonNull( + ReflectionUtils.getMethod( + clazz$Entity, new String[]{"j", "setInvisible"}, boolean.class + ) + ); + + public static final Class clazz$GameProfile = requireNonNull( + ReflectionUtils.getClazz("com.mojang.authlib.GameProfile") + ); + + public static final Constructor constructor$GameProfile = requireNonNull( + ReflectionUtils.getConstructor( + clazz$GameProfile, 0 + ) + ); + + public static final Method method$GameProfile$getProperties = requireNonNull( + ReflectionUtils.getMethod( + clazz$GameProfile, ForwardingMultimap.class + ) + ); + + public static final Constructor constructor$ServerPlayer = requireNonNull( + ReflectionUtils.getConstructor( + clazz$ServerPlayer, 0 + ) + ); + + public static final Method method$ServerPlayer$getGameProfile = requireNonNull( + ReflectionUtils.getMethod( + clazz$ServerPlayer, clazz$GameProfile, 0 + ) + ); + + // 1.20.2 + + public static final Method method$ServerPlayer$clientInformation = Optional.ofNullable(clazz$ClientInformation) + .map(it -> ReflectionUtils.getMethod(clazz$ServerPlayer, it, 0)) + .orElse(null); + + public static final Class clazz$CompoundTag = requireNonNull( + BukkitReflectionUtils.findReobfOrMojmapClass( + "nbt.NBTTagCompound", + "nbt.CompoundTag" + ) + ); + + public static final Object instance$CompoundTag$Empty; + + static { + try { + instance$CompoundTag$Empty = CoreReflections.clazz$CompoundTag.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to instantiate empty CompoundTag", e); + } + } + + public static final Method method$Entity$getEntityData = requireNonNull( + ReflectionUtils.getMethod( + clazz$Entity, clazz$SynchedEntityData, 0 + ) + ); + + public static final Method method$SynchedEntityData$set = requireNonNull( + ReflectionUtils.getMethod( + clazz$SynchedEntityData, void.class, clazz$EntityDataAccessor, Object.class + ) + ); + + public static final Method method$SynchedEntityData$isDirty = requireNonNull( + ReflectionUtils.getMethod( + clazz$SynchedEntityData, boolean.class + ) + ); + + public static final Method method$SynchedEntityData$packDirty = requireNonNull( + ReflectionUtils.getMethod( + clazz$SynchedEntityData, List.class, new String[]{"b", "packDirty"} + ) + ); + + public static final Class clazz$SynchedEntityData$DataItem = requireNonNull( + BukkitReflectionUtils.findReobfOrMojmapClass( + "network.syncher.DataWatcher$Item", + "network.syncher.SynchedEntityData$DataItem" + ) + ); + + public static final Method method$SynchedEntityData$getItem = requireNonNull( + ReflectionUtils.getDeclaredMethod( + clazz$SynchedEntityData, clazz$SynchedEntityData$DataItem, clazz$EntityDataAccessor) + ); + + public static final Method method$SynchedEntityData$DataItem$value = requireNonNull( + ReflectionUtils.getMethod( + clazz$SynchedEntityData$DataItem, clazz$SynchedEntityData$DataValue, 0 + ) + ); + + public static final Class clazz$RemoteChatSession$Data = requireNonNull( + BukkitReflectionUtils.findReobfOrMojmapClass( + "network.chat.RemoteChatSession$a", + "network.chat.RemoteChatSession$Data" + ) + ); + + public static final Method method$GameType$values = requireNonNull( + ReflectionUtils.getStaticMethod( + clazz$GameType, clazz$GameType.arrayType() + ) + ); + + public static final Object instance$GameType$SURVIVAL; + public static final Object instance$GameType$CREATIVE; + public static final Object instance$GameType$ADVENTURE; + public static final Object instance$GameType$SPECTATOR; + + static { + try { + Object[] values = (Object[]) method$GameType$values.invoke(null); + instance$GameType$SURVIVAL = values[0]; + instance$GameType$CREATIVE = values[1]; + instance$GameType$ADVENTURE = values[2]; + instance$GameType$SPECTATOR = values[3]; + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + // 1.21.3 + + public static final Class clazz$PositionMoveRotation = ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("world.entity.PositionMoveRotation") + ); + + public static final Method method$PositionMoveRotation$of = Optional.ofNullable(clazz$PositionMoveRotation) + .map(it -> ReflectionUtils.getStaticMethod(it, it, clazz$Entity)) + .orElse(null); + + public static final Method method$Entity$absSnapTo = requireNonNull( + ReflectionUtils.getMethod( + clazz$Entity, void.class, double.class, double.class, double.class, float.class, float.class + ) + ); + + public static final Method method$Entity$setSharedFlag = requireNonNull( + ReflectionUtils.getDeclaredMethod( + clazz$Entity, void.class, int.class, boolean.class + ) + ); + public static final Method method$Level$destroyBlock = requireNonNull( ReflectionUtils.getDeclaredMethod( clazz$Level, boolean.class, clazz$BlockPos, boolean.class, clazz$Entity, int.class diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/MItems.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/MItems.java index 51fbc836b..cf2ee270b 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/MItems.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/MItems.java @@ -2,6 +2,7 @@ package net.momirealms.craftengine.bukkit.plugin.reflection.minecraft; import net.momirealms.craftengine.bukkit.nms.FastNMS; import net.momirealms.craftengine.bukkit.plugin.reflection.ReflectionInitException; +import net.momirealms.craftengine.bukkit.util.ItemUtils; public final class MItems { private MItems() {} @@ -22,4 +23,6 @@ public final class MItems { throw new ReflectionInitException("Failed to init Items", e); } } + + public static final Object AIR$Item = FastNMS.INSTANCE.method$CraftItemStack$asNMSCopy(ItemUtils.AIR); } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/NetworkReflections.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/NetworkReflections.java index 4acedf0be..5da8fe365 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/NetworkReflections.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/reflection/minecraft/NetworkReflections.java @@ -178,6 +178,10 @@ public final class NetworkReflections { "network.protocol.game.ClientboundAddPlayerPacket" ); + public static final Constructor constructor$ClientboundAddPlayerPacket = Optional.ofNullable(clazz$ClientboundAddPlayerPacket) + .map(it -> ReflectionUtils.getConstructor(clazz$ClientboundAddPlayerPacket, CoreReflections.clazz$Player)) + .orElse(null); + public static final Class clazz$ClientboundRemoveEntitiesPacket = requireNonNull( BukkitReflectionUtils.findReobfOrMojmapClass( "network.protocol.game.PacketPlayOutEntityDestroy", @@ -382,12 +386,30 @@ public final class NetworkReflections { ReflectionUtils.getStaticMethod(clazz$ClientboundPlayerInfoUpdatePacket$Action, clazz$ClientboundPlayerInfoUpdatePacket$Action.arrayType()) ); + + public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$ADD_PLAYER; + public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$INITIALIZE_CHAT; + public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_GAME_MODE; + public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_LISTED; + public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_LATENCY; public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_DISPLAY_NAME; + //public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_LIST_ORDER; + //public static final Object instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_HAT; + static { try { Object[] values = (Object[]) method$ClientboundPlayerInfoUpdatePacket$Action$values.invoke(null); + instance$ClientboundPlayerInfoUpdatePacket$Action$ADD_PLAYER = values[0]; + instance$ClientboundPlayerInfoUpdatePacket$Action$INITIALIZE_CHAT = values[1]; + instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_GAME_MODE = values[2]; + instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_LISTED = values[3]; + instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_LATENCY = values[4]; instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_DISPLAY_NAME = values[5]; + //1.21.3 + //instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_LIST_ORDER = values[6]; + //1.21.4 + //instance$ClientboundPlayerInfoUpdatePacket$Action$UPDATE_HAT = values[7]; } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } @@ -1476,4 +1498,78 @@ public final class NetworkReflections { } } + + public static final Class clazz$ClientBoundPlayerInfoUpdatePacket$Entry = requireNonNull( + BukkitReflectionUtils.findReobfOrMojmapClass( + "network.protocol.game.ClientboundPlayerInfoUpdatePacket$b", + "network.protocol.game.ClientboundPlayerInfoUpdatePacket$Entry" + ) + ); + + public static final Constructor constructor$ClientBoundPlayerInfoUpdatePacket$Entry = requireNonNull( + VersionHelper.isOrAbove1_21_3() + ? VersionHelper.isOrAbove1_21_4() + ? ReflectionUtils.getConstructor(clazz$ClientBoundPlayerInfoUpdatePacket$Entry, UUID.class, CoreReflections.clazz$GameProfile, boolean.class, int.class, CoreReflections.clazz$GameType, CoreReflections.clazz$Component, boolean.class, int.class, CoreReflections.clazz$RemoteChatSession$Data) + : ReflectionUtils.getConstructor(clazz$ClientBoundPlayerInfoUpdatePacket$Entry, UUID.class, CoreReflections.clazz$GameProfile, boolean.class, int.class, CoreReflections.clazz$GameType, CoreReflections.clazz$Component, int.class, CoreReflections.clazz$RemoteChatSession$Data) + : ReflectionUtils.getConstructor(clazz$ClientBoundPlayerInfoUpdatePacket$Entry, UUID.class, CoreReflections.clazz$GameProfile, boolean.class, int.class, CoreReflections.clazz$GameType, CoreReflections.clazz$Component, CoreReflections.clazz$RemoteChatSession$Data) + ); + + public static final Class clazz$ClientboundTeleportEntityPacket = requireNonNull( + BukkitReflectionUtils.findReobfOrMojmapClass( + "network.protocol.game.PacketPlayOutEntityTeleport", + "network.protocol.game.ClientboundTeleportEntityPacket" + ) + ); + + public static final Constructor constructor$ClientboundTeleportEntityPacket = requireNonNull( + VersionHelper.isOrAbove1_21_3() + ? ReflectionUtils.getConstructor(clazz$ClientboundTeleportEntityPacket, int.class, CoreReflections.clazz$PositionMoveRotation, Set.class, boolean.class) + : ReflectionUtils.getConstructor(clazz$ClientboundTeleportEntityPacket, CoreReflections.clazz$Entity) + ); + + public static final Method method$ClientboundSetEquipmentPacket$getEntity = requireNonNull( + ReflectionUtils.getMethod( + NetworkReflections.clazz$ClientboundSetEquipmentPacket, int.class + ) + ); + + public static final Method method$ClientboundSetEquipmentPacket$getSlots = requireNonNull( + ReflectionUtils.getMethod( + NetworkReflections.clazz$ClientboundSetEquipmentPacket, List.class + ) + ); + + public static final Method method$ClientboundContainerSetSlotPacket$getContainerId = requireNonNull( + ReflectionUtils.getMethod( + clazz$ClientboundContainerSetSlotPacket, int.class, new String[]{"a", "getContainerId"} + ) + ); + + public static final Method method$ClientboundContainerSetSlotPacket$getSlot = requireNonNull( + ReflectionUtils.getMethod( + clazz$ClientboundContainerSetSlotPacket, int.class, new String[]{"c", "d", "getSlot"} + ) + ); + + public static final Method method$ClientboundContainerSetSlotPacket$getStateId = requireNonNull( + ReflectionUtils.getMethod( + clazz$ClientboundContainerSetSlotPacket, int.class, new String[]{"e", "f", "getStateId"} + ) + ); + + public static final Class clazz$ClientboundAnimatePacket = requireNonNull( + BukkitReflectionUtils.findReobfOrMojmapClass( + "network.protocol.game.PacketPlayOutAnimation", + "network.protocol.game.ClientboundAnimatePacket") + ); + + public static final Constructor constructor$ClientboundAnimatePacket = requireNonNull( + ReflectionUtils.getConstructor(clazz$ClientboundAnimatePacket, CoreReflections.clazz$Entity, int.class) + ); + + public static final Constructor constructor$ClientboundRotateHeadPacket = requireNonNull( + ReflectionUtils.getDeclaredConstructor( + clazz$ClientboundRotateHeadPacket, CoreReflections.clazz$Entity, byte.class + ) + ); } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java index d15259099..7bafab4b4 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java @@ -23,6 +23,7 @@ import net.momirealms.craftengine.core.block.ImmutableBlockState; import net.momirealms.craftengine.core.entity.player.GameMode; import net.momirealms.craftengine.core.entity.player.InteractionHand; import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; import net.momirealms.craftengine.core.item.Item; import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.config.Config; @@ -110,6 +111,8 @@ public class BukkitServerPlayer extends Player { private double cachedInteractionRange; // cooldown data private CooldownData cooldownData; + // cache seat + private SeatEntity seatEntity; private final Map entityTypeView = new ConcurrentHashMap<>(); @@ -976,4 +979,14 @@ public class BukkitServerPlayer extends Player { public CooldownData cooldown() { return this.cooldownData; } + + @Override + public void setSeat(SeatEntity seatEntity) { + this.seatEntity = seatEntity; + } + + @Override + public SeatEntity seat() { + return this.seatEntity; + } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/EntityUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/EntityUtils.java index 30fd6fa4b..3a23c5277 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/EntityUtils.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/EntityUtils.java @@ -2,6 +2,7 @@ package net.momirealms.craftengine.bukkit.util; import net.momirealms.craftengine.bukkit.nms.FastNMS; import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections; +import net.momirealms.craftengine.core.entity.EquipmentSlot; import net.momirealms.craftengine.core.util.VersionHelper; import net.momirealms.craftengine.core.world.BlockPos; import org.bukkit.Location; @@ -13,6 +14,9 @@ import org.bukkit.event.entity.CreatureSpawnEvent; import java.util.function.Consumer; +import static net.momirealms.craftengine.core.entity.EquipmentSlot.BODY; +import static net.momirealms.craftengine.core.entity.EquipmentSlot.MAIN_HAND; + public class EntityUtils { private EntityUtils() {} @@ -34,4 +38,41 @@ public class EntityUtils { return LegacyEntityUtils.spawnEntity(world, loc, type, function); } } + + public static org.bukkit.inventory.EquipmentSlot toBukkitEquipmentSlot(EquipmentSlot slot) { + return switch (slot) { + case MAIN_HAND -> org.bukkit.inventory.EquipmentSlot.HAND; + case OFF_HAND -> org.bukkit.inventory.EquipmentSlot.OFF_HAND; + case HEAD -> org.bukkit.inventory.EquipmentSlot.HEAD; + case CHEST -> org.bukkit.inventory.EquipmentSlot.CHEST; + case LEGS -> org.bukkit.inventory.EquipmentSlot.LEGS; + case FEET -> org.bukkit.inventory.EquipmentSlot.FEET; + default -> org.bukkit.inventory.EquipmentSlot.BODY; + }; + } + + public static EquipmentSlot toCEEquipmentSlot(org.bukkit.inventory.EquipmentSlot slot) { + return switch (slot) { + case HAND -> MAIN_HAND; + case OFF_HAND -> EquipmentSlot.OFF_HAND; + case HEAD -> EquipmentSlot.HEAD; + case CHEST -> EquipmentSlot.CHEST; + case LEGS -> EquipmentSlot.LEGS; + case FEET -> EquipmentSlot.FEET; + case BODY -> EquipmentSlot.BODY; + default -> BODY; + }; + } + + public static Object fromEquipmentSlot(EquipmentSlot slot) { + return switch (slot) { + case MAIN_HAND -> CoreReflections.instance$EquipmentSlot$MAINHAND; + case OFF_HAND -> CoreReflections.instance$EquipmentSlot$OFFHAND; + case HEAD -> CoreReflections.instance$EquipmentSlot$HEAD; + case CHEST -> CoreReflections.instance$EquipmentSlot$CHEST; + case LEGS -> CoreReflections.instance$EquipmentSlot$LEGS; + case FEET -> CoreReflections.instance$EquipmentSlot$FEET; + default -> new Object(); + }; + } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/ItemUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/ItemUtils.java index 1841169e8..46f8594e7 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/ItemUtils.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/ItemUtils.java @@ -7,6 +7,8 @@ import org.jetbrains.annotations.Contract; public class ItemUtils { + public static final ItemStack AIR = new ItemStack(Material.AIR); + private ItemUtils() {} @Contract("null -> true") diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java index 9a0c33285..189d9bc16 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java @@ -8,6 +8,7 @@ import net.momirealms.craftengine.bukkit.plugin.network.BukkitNetworkManager; import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections; import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.NetworkReflections; import net.momirealms.craftengine.core.util.RandomUtils; +import net.momirealms.craftengine.core.util.VersionHelper; import org.bukkit.Location; import org.bukkit.entity.Item; import org.bukkit.entity.Player; @@ -20,6 +21,7 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static java.util.Objects.requireNonNull; @@ -174,4 +176,8 @@ public final class PlayerUtils { BukkitCraftEngine.instance().logger().warn("Failed to send totem animation"); } } + + public static Set getTrackedBy(Player player) { + return VersionHelper.isOrAbove1_20_2() ? player.getTrackedBy() : player.getTrackedPlayers(); + } } diff --git a/common-files/src/main/resources/translations/en.yml b/common-files/src/main/resources/translations/en.yml index cdebefa6d..bf461eea9 100644 --- a/common-files/src/main/resources/translations/en.yml +++ b/common-files/src/main/resources/translations/en.yml @@ -152,6 +152,7 @@ warning.config.furniture.element.missing_item: "Issue found in file Issue found in file - The furniture '' is using an unknown setting type ''." warning.config.furniture.hitbox.invalid_type: "Issue found in file - The furniture '' is using an invalid hitbox type ''." warning.config.furniture.hitbox.custom.invalid_entity: "Issue found in file - The furniture '' is using a custom hitbox with invalid entity type ''." +warning.config.furniture.seat.invalid_type: "Issue found in file - The furniture '' is using an invalid seat type ''." warning.config.item.duplicate: "Issue found in file - Duplicated item ''. Please check if there is the same configuration in other files." warning.config.item.settings.unknown: "Issue found in file - The item '' is using an unknown setting type ''." warning.config.item.settings.invulnerable.invalid_damage_source: "Issue found in file - The item '' is using an unknown damage source ''. Allowed sources: []." diff --git a/common-files/src/main/resources/translations/zh_cn.yml b/common-files/src/main/resources/translations/zh_cn.yml index b3c62a34d..a019f50ec 100644 --- a/common-files/src/main/resources/translations/zh_cn.yml +++ b/common-files/src/main/resources/translations/zh_cn.yml @@ -65,6 +65,7 @@ command.send_resource_pack.success.single: "发送资源包给 发送资源包给 个玩家" warning.config.pack.duplicated_files: "发现重复文件 请通过 config.yml 的 'resource-pack.duplicated-files-handler' 部分解决" warning.config.yaml.duplicated_key: "在文件 发现问题 - 在第行发现重复的键 '', 这可能会导致一些意料之外的问题." +warning.config.yaml.key_path_conflict: "在文件 发现问题 - 在第行发现重复且值类型不同的键 '', 这可能会导致一些意料之外的问题." warning.config.type.int: "在文件 发现问题 - 无法加载 '': 无法将 '' 转换为整数类型 (选项 '')" warning.config.type.float: "在文件 发现问题 - 无法加载 '': 无法将 '' 转换为浮点数类型 (选项 '')" warning.config.type.boolean: "在文件 发现问题 - 无法加载 '': 无法将 '' 转换为布尔类型 (选项 '')" @@ -152,6 +153,7 @@ warning.config.furniture.element.missing_item: "在文件 发现 warning.config.furniture.settings.unknown: "在文件 发现问题 - 家具 '' 使用了未知的设置类型 ''" warning.config.furniture.hitbox.invalid_type: "在文件 发现问题 - 家具 '' 使用了无效的碰撞箱类型 ''" warning.config.furniture.hitbox.custom.invalid_entity: "在文件 发现问题 - 家具 '' 的自定义碰撞箱使用了无效的实体类型 ''" +warning.config.furniture.seat.invalid_type: "在文件 发现问题 - 家具 '' 使用了无效的座椅类型 ''" warning.config.item.duplicate: "在文件 发现问题 - 重复的物品 '' 请检查其他文件中是否存在相同配置" warning.config.item.settings.unknown: "在文件 发现问题 - 物品 '' 使用了未知的设置类型 ''" warning.config.item.settings.invulnerable.invalid_damage_source: "在文件 发现问题 - 物品 '' 物品使用了未知的伤害来源类型 '' 允许的来源: []" diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/EquipmentSlot.java b/core/src/main/java/net/momirealms/craftengine/core/entity/EquipmentSlot.java index 55c343103..f3f9c279a 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/entity/EquipmentSlot.java +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/EquipmentSlot.java @@ -8,5 +8,13 @@ public enum EquipmentSlot { BODY, MAIN_HAND, OFF_HAND, - SADDLE + SADDLE; + + public boolean isHand() { + return this == MAIN_HAND || this == OFF_HAND; + } + + public boolean isPlayerArmor() { + return this == HEAD || this == CHEST || this == LEGS || this == FEET; + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractSeat.java b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractSeat.java new file mode 100644 index 000000000..1f0624ba5 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractSeat.java @@ -0,0 +1,39 @@ +package net.momirealms.craftengine.core.entity.furniture; + +import org.joml.Vector3f; + +import java.util.Objects; + +public abstract class AbstractSeat implements Seat { + protected final Vector3f offset; + protected final float yaw; + + public AbstractSeat(Vector3f offset, float yaw) { + this.offset = offset; + this.yaw = yaw; + } + + @Override + public Vector3f offset() { + return offset; + } + + @Override + public float yaw() { + return yaw; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AbstractSeat seat)) return false; + return Float.compare(yaw, seat.yaw()) == 0 && offset.equals(seat.offset()); + } + + @Override + public int hashCode() { + int result = Objects.hash(offset); + result = 31 * result + Float.hashCode(yaw); + return result; + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/HitBoxFactory.java b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/HitBoxFactory.java index 3720eb9bd..2779a5d6c 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/HitBoxFactory.java +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/HitBoxFactory.java @@ -13,11 +13,7 @@ public interface HitBoxFactory { static Seat[] getSeats(Map arguments) { List seats = (List) arguments.getOrDefault("seats", List.of()); return seats.stream() - .map(arg -> { - String[] split = arg.split(" "); - if (split.length == 1) return new Seat(MiscUtils.getAsVector3f(split[0], "seats"), 0, false); - return new Seat(MiscUtils.getAsVector3f(split[0], "seats"), Float.parseFloat(split[1]), true); - }) + .map(SeatType::fromString) .toArray(Seat[]::new); } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/Seat.java b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/Seat.java index 885333703..deb77a736 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/Seat.java +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/Seat.java @@ -1,23 +1,14 @@ package net.momirealms.craftengine.core.entity.furniture; +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; import org.joml.Vector3f; -import java.util.Objects; +public interface Seat { -public record Seat(Vector3f offset, float yaw, boolean limitPlayerRotation) { + SeatEntity spawn(Player player, Furniture furniture); - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Seat seat)) return false; - return Float.compare(yaw, seat.yaw) == 0 && Objects.equals(offset, seat.offset) && limitPlayerRotation == seat.limitPlayerRotation; - } + Vector3f offset(); - @Override - public int hashCode() { - int result = Objects.hashCode(offset); - result = 31 * result + Float.hashCode(yaw); - result = 31 * result + Boolean.hashCode(limitPlayerRotation); - return result; - } + float yaw(); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/SeatFactory.java b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/SeatFactory.java new file mode 100644 index 000000000..e665eac51 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/SeatFactory.java @@ -0,0 +1,8 @@ +package net.momirealms.craftengine.core.entity.furniture; + +import java.util.List; + +public interface SeatFactory { + + Seat create(List args); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/SeatType.java b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/SeatType.java new file mode 100644 index 000000000..40dcc83ce --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/SeatType.java @@ -0,0 +1,60 @@ +package net.momirealms.craftengine.core.entity.furniture; + +import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; +import net.momirealms.craftengine.core.registry.BuiltInRegistries; +import net.momirealms.craftengine.core.registry.Holder; +import net.momirealms.craftengine.core.registry.Registries; +import net.momirealms.craftengine.core.registry.WritableRegistry; +import net.momirealms.craftengine.core.util.Key; +import net.momirealms.craftengine.core.util.ResourceKey; + +import java.util.ArrayList; +import java.util.List; + +public class SeatType { + public static final Key SIT = Key.of("craftengine:sit"); + public static final Key LAY = Key.of("craftengine:lay"); + public static final Key CRAWL = Key.of("craftengine:crawl"); + + public static void register(Key key, SeatFactory factory) { + Holder.Reference holder = ((WritableRegistry) BuiltInRegistries.SEAT_FACTORY) + .registerForHolder(new ResourceKey<>(Registries.SEAT_FACTORY.location(), key)); + holder.bindValue(factory); + } + + public static Seat fromString(String s) { + int lastSpaceIndex = s.lastIndexOf(' '); + + Key type = SIT; + SeatFactory factory; + String numericPart; + + if (lastSpaceIndex != -1) { + numericPart = s.substring(lastSpaceIndex + 1); + try { + Float.parseFloat(numericPart); + } catch (NumberFormatException e) { + type = Key.withDefaultNamespace(numericPart, "craftengine"); + s = s.substring(0, lastSpaceIndex); + lastSpaceIndex = s.lastIndexOf(' '); + } + } + + List split = new ArrayList<>(); + int start = 0; + while (lastSpaceIndex != -1) { + split.add(s.substring(start, lastSpaceIndex)); + start = lastSpaceIndex + 1; + lastSpaceIndex = s.indexOf(' ', start); + } + if (start < s.length()) { + split.add(s.substring(start)); + } + + factory = BuiltInRegistries.SEAT_FACTORY.getValue(type); + if (factory == null) { + throw new LocalizedResourceConfigException("warning.config.furniture.seat.invalid_type", type.toString()); + } + return factory.create(split); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java b/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java index e0b2d0394..51c141612 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java @@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.entity.player; import net.kyori.adventure.text.Component; import net.momirealms.craftengine.core.entity.AbstractEntity; +import net.momirealms.craftengine.core.entity.seat.SeatEntity; import net.momirealms.craftengine.core.item.Item; import net.momirealms.craftengine.core.plugin.context.CooldownData; import net.momirealms.craftengine.core.plugin.network.NetWorkUser; @@ -140,4 +141,8 @@ public abstract class Player extends AbstractEntity implements NetWorkUser { public abstract void clearPotionEffects(); public abstract CooldownData cooldown(); + + public abstract void setSeat(SeatEntity seatEntity); + + public abstract SeatEntity seat(); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/seat/SeatEntity.java b/core/src/main/java/net/momirealms/craftengine/core/entity/seat/SeatEntity.java new file mode 100644 index 000000000..033eab3cc --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/seat/SeatEntity.java @@ -0,0 +1,28 @@ +package net.momirealms.craftengine.core.entity.seat; + +import net.momirealms.craftengine.core.entity.furniture.Furniture; +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.plugin.network.EntityPacketHandler; +import net.momirealms.craftengine.core.plugin.network.NetWorkUser; +import org.joml.Vector3f; + +public interface SeatEntity extends EntityPacketHandler { + + void add(NetWorkUser to); + + void dismount(Player player); + + void onDismount(Player player); + + void event(Player player, Object event); + + void destroy(); + + boolean destroyed(); + + Furniture furniture(); + + Vector3f vector3f(); + + int playerID(); +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/StringKeyConstructor.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/StringKeyConstructor.java index bcca9168c..a54f63239 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/StringKeyConstructor.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/StringKeyConstructor.java @@ -1,3 +1,4 @@ + package net.momirealms.craftengine.core.plugin.config; import net.momirealms.craftengine.core.plugin.locale.TranslationManager; @@ -49,17 +50,15 @@ public class StringKeyConstructor extends SafeConstructor { */ @Override public Object constructObject(Node node) { - if (node instanceof MappingNode mappingNode) { - if (isValueSelectorNode(mappingNode)) { - // 场景B: 这是一个值选择器,解析它以获得单个值 - return constructVersionedValue(mappingNode); - } + if (node instanceof MappingNode mappingNode && isValueSelectorNode(mappingNode)) { + return constructVersionedValue(mappingNode); } // 对于所有其他情况 (包括需要合并的Map),使用默认的构造逻辑 // super.constructObject 会最终调用我们重写的 constructMapping return super.constructObject(node); } + /** * 场景A (块合并与路径展开): 构造一个Map,同时处理其中的版本化块合并和 `::` 分隔的深层键。 */ @@ -67,66 +66,124 @@ public class StringKeyConstructor extends SafeConstructor { @Override protected Map constructMapping(MappingNode node) { Map map = new LinkedHashMap<>(); + for (NodeTuple tuple : node.getValue()) { Node keyNode = tuple.getKeyNode(); if (!(keyNode instanceof ScalarNode)) continue; - - String key = constructScalar((ScalarNode) keyNode); Node valueNode = tuple.getValueNode(); - if (key.startsWith(VERSION_PREFIX)) { - // 处理版本化块合并 - String versionSpec = key.substring(VERSION_PREFIX.length()); - if (isVersionMatch(versionSpec)) { - if (valueNode instanceof MappingNode) { - // 将版本匹配的map内容合并到当前map - map.putAll(constructMapping((MappingNode) valueNode)); - } else { - logWarning("versioned_key_not_a_map", key, valueNode); - } - } - } else if (key.contains(DEEP_KEY_SEPARATOR)) { - // 处理 '::' 分隔的深层键 - String[] parts = key.split(DEEP_KEY_SEPARATOR); - Object value = constructObject(valueNode); - Map currentMap = map; + String key = constructScalar((ScalarNode) keyNode); - // 遍历除最后一个部分外的所有路径,创建嵌套的map - for (int i = 0; i < parts.length - 1; i++) { - String part = parts[i]; - Object nextObject = currentMap.get(part); - if (nextObject instanceof Map) { - currentMap = (Map) nextObject; - } else { - // 如果路径中存在一个非map的值,发出警告并覆盖它 - if (nextObject != null) { - logWarning("key_path_conflict", part, keyNode); - } - Map newMap = new LinkedHashMap<>(); - currentMap.put(part, newMap); - currentMap = newMap; - } - } - - // 在最深层的map中设置最终的键值对 - String finalKey = parts[parts.length - 1]; - Object previous = currentMap.put(finalKey, value); - if (previous != null) { - // 使用完整的原始键来报告重复键,更清晰 - logWarning("duplicated_key", key, keyNode); - } - } else { - // 原始逻辑:处理普通键 - Object value = constructObject(valueNode); - Object previous = map.put(key, value); - if (previous != null) { - logWarning("duplicated_key", key, keyNode); - } - } + // 处理 版本化块. + if (key.startsWith(VERSION_PREFIX)) processVersionedBlock(map, key, valueNode); + // 处理 深层键 -> {a::b::c: value} 和 {a::b: {c: value}}. + else if (key.contains(DEEP_KEY_SEPARATOR)) processDeepKey(map, key, valueNode, keyNode); + // 处理 正常键. + else processRegularKey(map, key, valueNode, keyNode); } + return map; } + + // 处理版本化块合并 + private void processVersionedBlock(Map targetMap, String key, Node valueNode) { + String versionSpec = key.substring(VERSION_PREFIX.length()); + + if (isVersionMatch(versionSpec)) { + if (valueNode instanceof MappingNode mappingNode) { + Map versionedMap = constructMapping(mappingNode); + mergeMap(targetMap, versionedMap, "", valueNode); + } else { + logWarning("versioned_key_not_a_map", key, valueNode); + } + } + } + + // 处理深层键 + private void processDeepKey(Map rootMap, String fullKey, Node valueNode, Node keyNode) { + // 分割出不同的层级 + String[] keyParts = fullKey.split(DEEP_KEY_SEPARATOR); + Map currentMap = rootMap; + + // 创建必要的的中间层级(最后一个key不应遍历, 如aa::bb::cc, 只应创建aa和bb.) + for (int i = 0; i < keyParts.length - 1; i++) { + String keyPart = keyParts[i]; + Object existingValue = currentMap.get(keyPart); + + // 路径中的值 + if (existingValue instanceof Map) { + currentMap = (Map) existingValue; + continue; + } + + // 如果路径中存在一个非map的值, 这意味着 + // 当存在了 {aa: bb}, 又想要写入 {aa::bb::c: value} 时, 会触发这个警告, 然后会覆盖之前的. + if (existingValue != null) logWarning("key_path_conflict", keyPart, keyNode); + + // 创建层级 + Map newMap = new LinkedHashMap<>(); + currentMap.put(keyPart, newMap); + currentMap = newMap; + } + + // 这里再处理最后的 cc key. + String finalKey = keyParts[keyParts.length - 1]; + Object newValue = constructObject(valueNode); + String keyPath = String.join(DEEP_KEY_SEPARATOR, keyParts); // 构建完整的键路径字符串 + + setValueWithDuplicationCheck(currentMap, finalKey, newValue, keyPath, keyNode); + } + + // 处理普通键 + private void processRegularKey(Map targetMap, String key, Node valueNode, Node keyNode) { + Object newValue = constructObject(valueNode); + setValueWithDuplicationCheck(targetMap, key, newValue, key, keyNode); + } + + + // 设置值并检查重复键 + @SuppressWarnings("unchecked") + private void setValueWithDuplicationCheck(Map targetMap, String key, Object newValue, String fullKeyPath, Node keyNode) { + Object existingValue = targetMap.get(key); + + if (existingValue == null) { + // 键不存在,直接设置. + targetMap.put(key, newValue); + } else if (existingValue instanceof Map && newValue instanceof Map) { + // 两个都是Map,直接合并. + mergeMap((Map) existingValue, (Map) newValue, fullKeyPath, keyNode); + } else { + // 存在重复键(至少一个不是Map) + logWarning("duplicated_key", fullKeyPath, keyNode); + targetMap.put(key, newValue); + } + } + + + // 合并两个Map并检查重复键 + @SuppressWarnings("unchecked") + private void mergeMap(Map target, Map source, String parentPath, Node sourceNode) { + for (Map.Entry entry : source.entrySet()) { + String key = entry.getKey().toString(); + Object sourceValue = entry.getValue(); + Object targetValue = target.get(key); + String currentPath = parentPath.isEmpty() ? key : parentPath + DEEP_KEY_SEPARATOR + key; + + // Map不存在该键,直接添加喵. + if (targetValue == null) target.put(key, sourceValue); + // 两个值都是Map,还需继续合并. + else if (targetValue instanceof Map && sourceValue instanceof Map) + mergeMap((Map) targetValue, (Map) sourceValue, currentPath, sourceNode); + // 发现重复的键, 爆炸了喵. + else { + logWarning("duplicated_key", currentPath, sourceNode); + target.put(key, sourceValue); + } + } + } + + /** * 检查一个MappingNode是否是“值选择器”(即所有键都以 '$$' 开头)。 */ diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/network/EntityPacketHandler.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/network/EntityPacketHandler.java index 15e0aa919..70113b8ca 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/network/EntityPacketHandler.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/network/EntityPacketHandler.java @@ -4,7 +4,7 @@ import it.unimi.dsi.fastutil.ints.IntList; public interface EntityPacketHandler { - default boolean handleEntitiesRemove(IntList entityIds) { + default boolean handleEntitiesRemove(NetWorkUser user, IntList entityIds) { return false; } @@ -19,4 +19,10 @@ public interface EntityPacketHandler { default void handleMove(NetWorkUser user, NMSPacketEvent event, Object packet) { } + + default void handleSetEquipment(NetWorkUser user, ByteBufPacketEvent event, Object slots) { + } + + default void handleContainerSetSlot(NetWorkUser user, NMSPacketEvent event, Object packet) { + } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/registry/BuiltInRegistries.java b/core/src/main/java/net/momirealms/craftengine/core/registry/BuiltInRegistries.java index 46c6061b6..6a31d24cb 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/registry/BuiltInRegistries.java +++ b/core/src/main/java/net/momirealms/craftengine/core/registry/BuiltInRegistries.java @@ -4,6 +4,7 @@ import net.momirealms.craftengine.core.block.CustomBlock; import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory; import net.momirealms.craftengine.core.block.properties.PropertyFactory; import net.momirealms.craftengine.core.entity.furniture.HitBoxFactory; +import net.momirealms.craftengine.core.entity.furniture.SeatFactory; import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory; import net.momirealms.craftengine.core.item.recipe.CustomSmithingTransformRecipe; import net.momirealms.craftengine.core.item.recipe.RecipeFactory; @@ -68,6 +69,7 @@ public class BuiltInRegistries { public static final Registry> EVENT_FUNCTION_FACTORY = createRegistry(Registries.EVENT_FUNCTION_FACTORY); public static final Registry> EVENT_CONDITION_FACTORY = createRegistry(Registries.EVENT_CONDITION_FACTORY); public static final Registry> PLAYER_SELECTOR_FACTORY = createRegistry(Registries.PLAYER_SELECTOR_FACTORY); + public static final Registry SEAT_FACTORY = createRegistry(Registries.SEAT_FACTORY); private static Registry createRegistry(ResourceKey> key) { return new MappedRegistry<>(key); diff --git a/core/src/main/java/net/momirealms/craftengine/core/registry/Registries.java b/core/src/main/java/net/momirealms/craftengine/core/registry/Registries.java index c050b9642..f2080b665 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/registry/Registries.java +++ b/core/src/main/java/net/momirealms/craftengine/core/registry/Registries.java @@ -4,6 +4,7 @@ import net.momirealms.craftengine.core.block.CustomBlock; import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory; import net.momirealms.craftengine.core.block.properties.PropertyFactory; import net.momirealms.craftengine.core.entity.furniture.HitBoxFactory; +import net.momirealms.craftengine.core.entity.furniture.SeatFactory; import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory; import net.momirealms.craftengine.core.item.recipe.CustomSmithingTransformRecipe; import net.momirealms.craftengine.core.item.recipe.RecipeFactory; @@ -69,4 +70,5 @@ public class Registries { public static final ResourceKey>> EVENT_FUNCTION_FACTORY = new ResourceKey<>(ROOT_REGISTRY, Key.withDefaultNamespace("event_function_factory")); public static final ResourceKey>> EVENT_CONDITION_FACTORY = new ResourceKey<>(ROOT_REGISTRY, Key.withDefaultNamespace("event_condition_factory")); public static final ResourceKey>> PLAYER_SELECTOR_FACTORY = new ResourceKey<>(ROOT_REGISTRY, Key.withDefaultNamespace("player_selector")); + public static final ResourceKey> SEAT_FACTORY = new ResourceKey<>(ROOT_REGISTRY, Key.withDefaultNamespace("seat_factory")); }