An unhinged investigation of /lay and why it might've broken
Fueled by a free Friday and severe lack of sleep
Fueled by a free Friday and severe lack of sleep
Okay! This will be a very low effort post, mostly for me to talk about GSit, the plug-in behind /lay and having a free Friday to come up with possible reasons on why it broke on SRP.
Don't worry if you aren't technical! This is intended to be a fun post where you can watch someone in their early 20s melting down over nothing in particular.
If you really aren't into code and such and just want to see my speculation on why, feel free to just scroll straight down! My findings are at the bottom.
Disclaimer I am NOT a plugin developer, and I am in no way qualified to talk about this. Please take everything in this post with a grain of salt
With that out of the way, let's begin!
The :sparkles: source :sparkles: code
To understand what even is going on, we need to dive into GSit and its wonderful code. My first impression going into this was:
"How bad could it be? It's just a plugin that makes you sit, seems simple"
I was wrong. So so wrong.
Let's go through this mess together. But first, I'd just like to say, this code is insanely sophisticated. I pride myself in being able to jump in to any piece of code and mostly get it in half an hour, but this thing is a different beast. Like, seriously,
Just.. what.. does this mean??
// What does any of these tags even correspond to???
Java:
playerNpc.getEntityData().set(EntityDataSerializers.POSE.createAccessor(6), net.minecraft.world.entity.Pose.values()[pose.ordinal()]);
if(pose == Pose.SPIN_ATTACK) playerNpc.getEntityData().set(EntityDataSerializers.BYTE.createAccessor(8), (byte) 4);
if(pose == Pose.SLEEPING) playerNpc.getEntityData().set(EntityDataSerializers.OPTIONAL_BLOCK_POS.createAccessor(14), Optional.of(bedPos));
playerNpc.getEntityData().set(EntityDataSerializers.BYTE.createAccessor(17), serverPlayer.getEntityData().get(EntityDataSerializers.BYTE.createAccessor(17)));
playerNpc.getEntityData().set(EntityDataSerializers.BYTE.createAccessor(18), serverPlayer.getEntityData().get(EntityDataSerializers.BYTE.createAccessor(18)));
playerNpc.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(19), serverPlayer.getEntityData().get(EntityDataSerializers.COMPOUND_TAG.createAccessor(19)));
playerNpc.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(20), serverPlayer.getEntityData().get(EntityDataSerializers.COMPOUND_TAG.createAccessor(20)));
serverPlayer.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(19), new CompoundTag());
serverPlayer.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(20), new CompoundTag());
But there is no point in despairing. Let's check the code files.. oh..
Forget that plan. Let's just.. look at the closest thing to lay, and would you look at that! Theres a convenient `GLayCommand.java`. It probably is about /lay, let's dig into it
Java:
package dev.geco.gsit.cmd;
import org.jetbrains.annotations.*;
import org.bukkit.*;
import org.bukkit.block.*;
import org.bukkit.command.*;
import org.bukkit.entity.*;
import org.bukkit.util.*;
import dev.geco.gsit.GSitMain;
import dev.geco.gsit.objects.*;
public class GLayCommand implements CommandExecutor {
private final GSitMain GPM;
public GLayCommand(GSitMain GPluginMain) { GPM = GPluginMain; }
@Override
public boolean onCommand(@NotNull CommandSender Sender, @NotNull Command Command, @NotNull String Label, String[] Args) {
if(!(Sender instanceof Player player)) {
GPM.getMManager().sendMessage(Sender, "Messages.command-sender-error");
return true;
}
if(!GPM.getPManager().hasPermission(Sender, "Lay", "Pose.*")) {
GPM.getMManager().sendMessage(Sender, "Messages.command-permission-error");
return true;
}
if(!GPM.getPoseManager().isAvailable()) {
GPM.getMManager().sendMessage(Sender, "Messages.command-version-error", "%Version%", GPM.getSVManager().getServerVersion());
return true;
}
IGPoseSeat currentPoseSeat = GPM.getPoseManager().getPose(player);
if(currentPoseSeat != null && currentPoseSeat.getPose() == Pose.SLEEPING) {
GPM.getPoseManager().removePose(player, GetUpReason.GET_UP);
return true;
}
if(!player.isValid() || player.isSneaking() || !player.isOnGround() || player.getVehicle() != null || player.isSleeping()) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-now-error");
return true;
}
if(!GPM.getEnvironmentUtil().isInAllowedWorld(player)) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-world-error");
return true;
}
Location playerLocation = player.getLocation();
Block block = playerLocation.getBlock().isPassable() ? playerLocation.subtract(0, 0.0625, 0).getBlock() : playerLocation.getBlock();
if(GPM.getCManager().MATERIALBLACKLIST.contains(block.getType())) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-location-error");
return true;
}
boolean overSize = false;
try {
for(BoundingBox boundingBox : block.getCollisionShape().getBoundingBoxes()) if(boundingBox.getMaxY() > 1.25) overSize = true;
} catch (Throwable ignored) { }
if(!GPM.getCManager().ALLOW_UNSAFE && !(block.getRelative(BlockFace.UP).isPassable() && !overSize && (!block.isPassable() || !GPM.getCManager().CENTER_BLOCK))) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-location-error");
return true;
}
if(!GPM.getPManager().hasPermission(Sender, "ByPass.Region", "ByPass.*")) {
if(GPM.getWorldGuardLink() != null && !GPM.getWorldGuardLink().checkFlag(block.getLocation(), GPM.getWorldGuardLink().getFlag("pose"))) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-region-error");
return true;
}
if(GPM.getGriefPreventionLink() != null && !GPM.getGriefPreventionLink().check(block.getLocation(), player)) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-region-error");
return true;
}
if(GPM.getPlotSquaredLink() != null && !GPM.getPlotSquaredLink().canCreateSeat(block.getLocation(), player)) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-region-error");
return true;
}
}
if(!GPM.getCManager().SAME_BLOCK_REST && !GPM.getPoseManager().kickPose(block, player)) {
GPM.getMManager().sendMessage(Sender, "Messages.action-pose-kick-error");
return true;
}
if(GPM.getPoseManager().createPose(block, player, Pose.SLEEPING) == null) GPM.getMManager().sendMessage(Sender, "Messages.action-pose-error");
return true;
}
}
May god have mercy on my soul. Take in a few deep breaths, there's something here that we can use.
After a few excruciating minutes, we can find what we want
Java:
if(GPM.getPoseManager().createPose(block, player, Pose.SLEEPING) == null) GPM.getMManager().sendMessage(Sender, "Messages.action-pose-error");
That's it! Whatever this `.createPose` is, that's our way in to understand this. One problem, though.. what.. is a `.getPoseManager()`?
Oh..
Java:
package dev.geco.gsit.manager;
import java.util.*;
import org.bukkit.*;
import org.bukkit.block.*;
import org.bukkit.entity.*;
import dev.geco.gsit.GSitMain;
import dev.geco.gsit.api.event.*;
import dev.geco.gsit.objects.*;
import dev.geco.gsit.util.*;
public class PoseManager {
public static final String POSE_TAG = GSitMain.NAME + "_POSE";
private final GSitMain GPM;
private final boolean available;
private int pose_used = 0;
private long pose_used_nano = 0;
private final List<IGPoseSeat> poses = new ArrayList<>();
public PoseManager(GSitMain GPluginMain) {
GPM = GPluginMain;
available = GPM.getSVManager().isNewerOrVersion(18, 0);
}
public boolean isAvailable() { return available; }
public int getPoseUsedCount() { return pose_used; }
public long getPoseUsedSeconds() { return pose_used_nano / 1_000_000_000; }
public void resetFeatureUsedCount() {
pose_used = 0;
pose_used_nano = 0;
}
public List<IGPoseSeat> getPoses() { return new ArrayList<>(poses); }
public boolean isPosing(Player Player) { return getPose(Player) != null; }
public IGPoseSeat getPose(Player Player) { return poses.stream().filter(pose -> Player.equals(pose.getPlayer())).findFirst().orElse(null); }
public void clearPoses() { for(IGPoseSeat pose : getPoses()) removePose(pose.getPlayer(), GetUpReason.PLUGIN); }
public boolean isPoseBlock(Block Block) { return poses.stream().anyMatch(pose -> Block.equals(pose.getSeat().getBlock())); }
public List<IGPoseSeat> getPoses(Block Block) { return poses.stream().filter(pose -> Block.equals(pose.getSeat().getBlock())).toList(); }
public List<IGPoseSeat> getPoses(List<Block> Blocks) { return poses.stream().filter(pose -> Blocks.contains(pose.getSeat().getBlock())).toList(); }
public boolean kickPose(Block Block, Player Player) {
if(!isPoseBlock(Block)) return true;
if(!GPM.getPManager().hasPermission(Player, "Kick.Pose", "Kick.*")) return false;
for(IGPoseSeat p : getPoses(Block)) if(!removePose(p.getPlayer(), GetUpReason.KICKED)) return false;
return true;
}
public IGPoseSeat createPose(Block Block, Player Player, Pose Pose) { return createPose(Block, Player, Pose, 0d, 0d, 0d, Player.getLocation().getYaw(), GPM.getCManager().CENTER_BLOCK); }
public IGPoseSeat createPose(Block Block, Player Player, Pose Pose, double XOffset, double YOffset, double ZOffset, float SeatRotation, boolean SitAtBlock) {
Location returnLocation = Player.getLocation();
Location seatLocation = GPM.getSitManager().getSeatLocation(Block, returnLocation, XOffset, YOffset, ZOffset, SitAtBlock);
if(!GPM.getEntityUtil().isLocationValid(seatLocation)) return null;
PrePlayerPoseEvent preEvent = new PrePlayerPoseEvent(Player, Block);
Bukkit.getPluginManager().callEvent(preEvent);
if(preEvent.isCancelled()) return null;
seatLocation.setYaw(SeatRotation);
Entity seatEntity = GPM.getEntityUtil().createSeatEntity(seatLocation, Player, true);
if(seatEntity == null) return null;
if(GPM.getCManager().CUSTOM_MESSAGE) {
GPM.getMManager().sendActionBarMessage(Player, "Messages.action-pose-info");
if(GPM.getCManager().ENHANCED_COMPATIBILITY) {
GPM.getTManager().runDelayed(() -> {
GPM.getMManager().sendActionBarMessage(Player, "Messages.action-pose-info");
}, Player, 2);
}
}
IGPoseSeat poseSeat = GPM.getEntityUtil().createPoseSeatObject(new GSeat(Block, seatLocation, Player, seatEntity, returnLocation), Pose);
poseSeat.spawn();
poses.add(poseSeat);
pose_used++;
Bukkit.getPluginManager().callEvent(new PlayerPoseEvent(poseSeat));
return poseSeat;
}
public boolean removePose(Player Player, GetUpReason Reason) { return removePose(Player, Reason, true); }
public boolean removePose(Player Player, GetUpReason Reason, boolean Safe) {
IGPoseSeat poseSeat = getPose(Player);
if(poseSeat == null) return true;
PrePlayerGetUpPoseEvent preEvent = new PrePlayerGetUpPoseEvent(poseSeat, Reason);
Bukkit.getPluginManager().callEvent(preEvent);
if(preEvent.isCancelled()) return false;
poses.remove(poseSeat);
poseSeat.remove();
Location returnLocation = GPM.getCManager().GET_UP_RETURN ? poseSeat.getSeat().getReturn() : poseSeat.getSeat().getLocation().add(0d, GPM.getSitManager().BASE_OFFSET + (Tag.STAIRS.isTagged(poseSeat.getSeat().getBlock().getType()) ? EnvironmentUtil.STAIR_Y_OFFSET : 0d) - GPM.getCManager().S_SITMATERIALS.getOrDefault(poseSeat.getSeat().getBlock().getType(), 0d), 0d);
Location entityLocation = Player.getLocation();
returnLocation.setYaw(entityLocation.getYaw());
returnLocation.setPitch(entityLocation.getPitch());
if(Player.isValid() && Safe) GPM.getEntityUtil().setEntityLocation(Player, returnLocation);
poseSeat.getSeat().getSeatEntity().remove();
Bukkit.getPluginManager().callEvent(new PlayerGetUpPoseEvent(poseSeat, Reason));
pose_used_nano += poseSeat.getSeat().getNano();
return true;
}
}
Yeah, I'd like to cry right about now. But we can't falter, there's something in here we are specifically looking for, it's the `.createPose` method
And.. voila! In that pile of code, we found this useful thing to continue our search
Java:
IGPoseSeat poseSeat = GPM.getEntityUtil().createPoseSeatObject(new GSeat(Block, seatLocation, Player, seatEntity, returnLocation), Pose);
So.. now we have a whole new problem. What is a `.getEntityUtil()`? Why! I am glad you asked. Thing is, this thing is a little special, it's different for every version of minecraft.
For the sake of all our sanity, let's stick to 1.21.1. In that version, that thing is:
Java:
package dev.geco.gsit.mcv.v1_21_2.objects;
import java.util.*;
import com.mojang.authlib.*;
import com.mojang.datafixers.util.*;
import org.bukkit.*;
import org.bukkit.event.*;
import org.bukkit.event.entity.*;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.*;
import org.bukkit.entity.*;
import org.bukkit.inventory.*;
import org.bukkit.craftbukkit.*;
import org.bukkit.craftbukkit.entity.*;
import net.minecraft.core.*;
import net.minecraft.nbt.*;
import net.minecraft.network.protocol.*;
import net.minecraft.network.protocol.game.*;
import net.minecraft.network.syncher.*;
import net.minecraft.server.*;
import net.minecraft.server.level.*;
import net.minecraft.world.effect.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.properties.*;
import dev.geco.gsit.GSitMain;
import dev.geco.gsit.manager.*;
import dev.geco.gsit.objects.*;
public class GPoseSeat implements IGPoseSeat {
private final GSitMain GPM = GSitMain.getInstance();
private final GSeat seat;
private final Player seatPlayer;
private final Pose pose;
private List<Player> nearPlayers = new ArrayList<>();
private final ServerPlayer serverPlayer;
protected final ServerPlayer playerNpc;
private final Location blockLocation;
private final org.bukkit.block.Block bedBlock;
private final BlockPos bedPos;
private final Direction direction;
protected ClientboundBlockUpdatePacket setBedPacket;
protected ClientboundPlayerInfoUpdatePacket addNpcInfoPacket;
protected ClientboundPlayerInfoRemovePacket removeNpcInfoPacket;
protected ClientboundRemoveEntitiesPacket removeNpcPacket;
protected ClientboundAddEntityPacket createNpcPacket;
protected ClientboundSetEntityDataPacket metaNpcPacket;
protected ClientboundUpdateAttributesPacket attributeNpcPacket;
protected ClientboundTeleportEntityPacket teleportNpcPacket;
protected ClientboundMoveEntityPacket.PosRot rotateNpcPacket;
protected ClientboundBundlePacket bundle;
private NonNullList<net.minecraft.world.item.ItemStack> equipmentSlotCache;
private net.minecraft.world.item.ItemStack mainSlotCache;
private float directionCache;
protected int renderRange;
private final Listener listener;
public GPoseSeat(GSeat Seat, Pose Pose) {
seat = Seat;
seatPlayer = (Player) Seat.getEntity();
pose = Pose;
Location seatLocation = seat.getLocation();
serverPlayer = ((CraftPlayer) seatPlayer).getHandle();
renderRange = seatPlayer.getWorld().getSimulationDistance() * 16;
playerNpc = createNPC();
double offset = seatLocation.getY() + GPM.getSitManager().BASE_OFFSET;
double scale = serverPlayer.getScale();
if(pose == org.bukkit.entity.Pose.SLEEPING) offset += 0.1125d * scale;
if(pose == org.bukkit.entity.Pose.SWIMMING) offset += -0.19 * scale;
playerNpc.moveTo(seatLocation.getX(), offset, seatLocation.getZ(), 0f, 0f);
blockLocation = seatLocation.clone();
blockLocation.setY(blockLocation.getWorld().getMinHeight());
bedBlock = blockLocation.getBlock();
bedPos = new BlockPos(blockLocation.getBlockX(), blockLocation.getBlockY(), blockLocation.getBlockZ());
direction = getDirection();
if(pose == org.bukkit.entity.Pose.SLEEPING) setBedPacket = new ClientboundBlockUpdatePacket(bedPos, Blocks.WHITE_BED.defaultBlockState().setValue(BedBlock.FACING, direction.getOpposite()).setValue(BedBlock.PART, BedPart.HEAD));
addNpcInfoPacket = new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), Collections.singletonList(playerNpc));
removeNpcInfoPacket = new ClientboundPlayerInfoRemovePacket(Collections.singletonList(playerNpc.getUUID()));
removeNpcPacket = new ClientboundRemoveEntitiesPacket(playerNpc.getId());
createNpcPacket = new ClientboundAddEntityPacket(playerNpc.getId(), playerNpc.getUUID(), playerNpc.getX(), playerNpc.getY(), playerNpc.getZ(), playerNpc.getXRot(), playerNpc.getYRot(), playerNpc.getType(), 0, playerNpc.getDeltaMovement(), playerNpc.getYHeadRot());
if(pose == org.bukkit.entity.Pose.SLEEPING) teleportNpcPacket = new ClientboundTeleportEntityPacket(playerNpc.getId(), net.minecraft.world.entity.PositionMoveRotation.of(playerNpc), Set.of(), false);
if(pose == org.bukkit.entity.Pose.SPIN_ATTACK) rotateNpcPacket = new ClientboundMoveEntityPacket.PosRot(playerNpc.getId(), (short) 0, (short) 0, (short) 0, (byte) 0, getFixedRotation(-90f), true);
listener = new Listener() {
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void PIntE(PlayerInteractEvent Event) { if(Event.getPlayer() == seatPlayer && !GPM.getCManager().P_INTERACT) Event.setCancelled(true); }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void PIntE(PlayerInteractEntityEvent Event) { if(Event.getPlayer() == seatPlayer) Event.setCancelled(true); }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void EDamBEE(EntityDamageByEntityEvent Event) { if(Event.getDamager() == seatPlayer && !GPM.getCManager().P_INTERACT) Event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void EDamE(EntityDamageEvent Event) { if(Event.getEntity() == seatPlayer) playAnimation(1); }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void PLauE(ProjectileLaunchEvent Event) { if(Event.getEntity().getShooter() == seatPlayer && !GPM.getCManager().P_INTERACT) Event.setCancelled(true); }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void PAniE(PlayerAnimationEvent Event) { if(Event.getPlayer() == seatPlayer && Event.getAnimationType() == PlayerAnimationType.ARM_SWING) playAnimation(Event.getPlayer().getMainHand().equals(MainHand.RIGHT) ? 0 : 3); }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void ICliE(InventoryClickEvent Event) { if(Event.getWhoClicked() == seatPlayer && seatPlayer.getGameMode() == GameMode.CREATIVE) Event.setCancelled(true); }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void PDroIE(PlayerDropItemEvent Event) { if(Event.getPlayer() == seatPlayer && seatPlayer.getGameMode() == GameMode.CREATIVE) Event.setCancelled(true); }
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void EPotEE(EntityPotionEffectEvent Event) { if(Event.getEntity() == seatPlayer) serverPlayer.setInvisible(true); }
};
}
@Override
public void spawn() {
nearPlayers = getNearPlayers();
playerNpc.setGlowingTag(serverPlayer.hasGlowingTag());
if(serverPlayer.hasGlowingTag()) serverPlayer.setGlowingTag(false);
playerNpc.getEntityData().set(EntityDataSerializers.POSE.createAccessor(6), net.minecraft.world.entity.Pose.values()[pose.ordinal()]);
if(pose == Pose.SPIN_ATTACK) playerNpc.getEntityData().set(EntityDataSerializers.BYTE.createAccessor(8), (byte) 4);
if(pose == Pose.SLEEPING) playerNpc.getEntityData().set(EntityDataSerializers.OPTIONAL_BLOCK_POS.createAccessor(14), Optional.of(bedPos));
playerNpc.getEntityData().set(EntityDataSerializers.BYTE.createAccessor(17), serverPlayer.getEntityData().get(EntityDataSerializers.BYTE.createAccessor(17)));
playerNpc.getEntityData().set(EntityDataSerializers.BYTE.createAccessor(18), serverPlayer.getEntityData().get(EntityDataSerializers.BYTE.createAccessor(18)));
playerNpc.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(19), serverPlayer.getEntityData().get(EntityDataSerializers.COMPOUND_TAG.createAccessor(19)));
playerNpc.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(20), serverPlayer.getEntityData().get(EntityDataSerializers.COMPOUND_TAG.createAccessor(20)));
serverPlayer.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(19), new CompoundTag());
serverPlayer.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(20), new CompoundTag());
serverPlayer.setInvisible(true);
setEquipmentVisibility(false);
if(pose == Pose.SLEEPING) {
if(GPM.getCManager().P_LAY_NIGHT_SKIP) seatPlayer.setSleepingIgnored(true);
if(GPM.getCManager().P_LAY_REST) seatPlayer.setStatistic(Statistic.TIME_SINCE_REST, 0);
}
metaNpcPacket = new ClientboundSetEntityDataPacket(playerNpc.getId(), playerNpc.getEntityData().isDirty() ? playerNpc.getEntityData().packDirty() : playerNpc.getEntityData().getNonDefaultValues());
attributeNpcPacket = new ClientboundUpdateAttributesPacket(playerNpc.getId(), serverPlayer.getAttributes().getSyncableAttributes());
List<Packet<? super ClientGamePacketListener>> packages = new ArrayList<>();
packages.add(addNpcInfoPacket);
packages.add(createNpcPacket);
if(pose == Pose.SLEEPING) packages.add(setBedPacket);
packages.add(metaNpcPacket);
packages.add(attributeNpcPacket);
if(pose == Pose.SLEEPING) {
packages.add(teleportNpcPacket);
packages.add(teleportNpcPacket);
}
if(pose == Pose.SPIN_ATTACK) packages.add(rotateNpcPacket);
bundle = new ClientboundBundlePacket(packages);
for(Player nearPlayer : nearPlayers) spawnToPlayer(nearPlayer);
PlayerSeatEntity placeholderEntity = new PlayerSeatEntity(seatPlayer.getLocation());
placeholderEntity.setVehicle(playerNpc);
List<Packet<? super ClientGamePacketListener>> playerPackages = new ArrayList<>();
playerPackages.add(new ClientboundAddEntityPacket(placeholderEntity.getId(), placeholderEntity.getUUID(), placeholderEntity.getX(), placeholderEntity.getY(), placeholderEntity.getZ(), placeholderEntity.getXRot(), placeholderEntity.getYRot(), placeholderEntity.getType(), 0, placeholderEntity.getDeltaMovement(), placeholderEntity.getYHeadRot()));
playerPackages.add(new ClientboundSetEntityDataPacket(placeholderEntity.getId(), placeholderEntity.getEntityData().getNonDefaultValues()));
playerPackages.add(new ClientboundSetPassengersPacket(playerNpc));
sendPacket(serverPlayer, new ClientboundBundlePacket(playerPackages));
Bukkit.getPluginManager().registerEvents(listener, GPM);
((SeatEntity) ((CraftEntity) seat.getSeatEntity()).getHandle()).setCallback(() -> {
List<Player> playerList = getNearPlayers();
for(Player nearPlayer : playerList) {
if(nearPlayers.contains(nearPlayer)) continue;
nearPlayers.add(nearPlayer);
spawnToPlayer(nearPlayer);
}
for(Player nearPlayer : new ArrayList<>(nearPlayers)) {
if(playerList.contains(nearPlayer)) continue;
nearPlayers.remove(nearPlayer);
removeToPlayer(nearPlayer);
}
if(pose != Pose.SPIN_ATTACK) updateDirection();
serverPlayer.setInvisible(true);
updateEquipment();
setEquipmentVisibility(false);
updateSkin();
if(pose != Pose.SLEEPING || !GPM.getCManager().P_LAY_SNORING_SOUNDS) return;
long tick = serverPlayer.getPlayerTime();
if((!GPM.getCManager().P_LAY_SNORING_NIGHT_ONLY || (tick >= 12500 && tick <= 23500)) && tick % 90 == 0) {
for(Player nearPlayer : nearPlayers) nearPlayer.playSound(seat.getLocation(), Sound.ENTITY_FOX_SLEEP, SoundCategory.PLAYERS, 1.5f, 0);
}
});
}
private void spawnToPlayer(Player Player) { sendPacket(Player, bundle); }
@Override
public void remove() {
((SeatEntity) ((CraftEntity) seat.getSeatEntity()).getHandle()).setCallback(null);
HandlerList.unregisterAll(listener);
seatPlayer.removeScoreboardTag(PoseManager.POSE_TAG);
for(Player nearPlayer : nearPlayers) removeToPlayer(nearPlayer);
if(pose == Pose.SLEEPING && GPM.getCManager().P_LAY_NIGHT_SKIP) seatPlayer.setSleepingIgnored(false);
serverPlayer.setInvisible(false);
setEquipmentVisibility(true);
serverPlayer.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(19), playerNpc.getEntityData().get(EntityDataSerializers.COMPOUND_TAG.createAccessor(19)));
serverPlayer.getEntityData().set(EntityDataSerializers.COMPOUND_TAG.createAccessor(20), playerNpc.getEntityData().get(EntityDataSerializers.COMPOUND_TAG.createAccessor(20)));
serverPlayer.setGlowingTag(playerNpc.hasGlowingTag());
}
private void removeToPlayer(Player Player) {
ServerPlayer removePlayer = ((CraftPlayer) Player).getHandle();
sendPacket(removePlayer, removeNpcInfoPacket);
sendPacket(removePlayer, removeNpcPacket);
Player.sendBlockChange(blockLocation, bedBlock.getBlockData());
}
private List<Player> getNearPlayers() {
List<Player> playerList = new ArrayList<>();
seatPlayer.getWorld().getPlayers().stream().filter(player -> seat.getLocation().distance(player.getLocation()) <= renderRange && player.canSee(seatPlayer)).forEach(playerList::add);
return playerList;
}
private float fixYaw(float Yaw) { return (Yaw < 0f ? 360f + Yaw : Yaw) % 360f; }
private void updateDirection() {
if(pose == Pose.SWIMMING) {
byte fixedRotation = getFixedRotation(seatPlayer.getLocation().getYaw());
if(directionCache == fixedRotation) return;
directionCache = fixedRotation;
ClientboundRotateHeadPacket rotateHeadPacket = new ClientboundRotateHeadPacket(playerNpc, fixedRotation);
ClientboundMoveEntityPacket.PosRot moveEntityPacket = new ClientboundMoveEntityPacket.PosRot(playerNpc.getId(), (short) 0, (short) 0, (short) 0, fixedRotation, (byte) 0, true);
for(Player nearPlayer : nearPlayers) {
ServerPlayer player = ((CraftPlayer) nearPlayer).getHandle();
sendPacket(player, rotateHeadPacket);
sendPacket(player, moveEntityPacket);
}
return;
}
float playerYaw = seatPlayer.getLocation().getYaw();
if(directionCache == playerYaw) return;
directionCache = playerYaw;
if(direction == Direction.WEST) playerYaw -= 90;
if(direction == Direction.EAST) playerYaw += 90;
if(direction == Direction.NORTH) playerYaw -= 180;
playerYaw = fixYaw(playerYaw);
byte fixedRotation = getFixedRotation(playerYaw >= 315 ? playerYaw - 360 : playerYaw <= 45 ? playerYaw : playerYaw >= 180 ? -45 : 45);
ClientboundRotateHeadPacket rotateHeadPacket = new ClientboundRotateHeadPacket(playerNpc, fixedRotation);
for(Player nearPlayer : nearPlayers) sendPacket(nearPlayer, rotateHeadPacket);
}
private void updateSkin() {
playerNpc.setInvisible(serverPlayer.activeEffects.containsKey(MobEffects.INVISIBILITY));
SynchedEntityData entityData = playerNpc.getEntityData();
entityData.set(EntityDataSerializers.BYTE.createAccessor(17), serverPlayer.getEntityData().get(EntityDataSerializers.BYTE.createAccessor(17)));
entityData.set(EntityDataSerializers.BYTE.createAccessor(18), serverPlayer.getEntityData().get(EntityDataSerializers.BYTE.createAccessor(18)));
if(!entityData.isDirty()) return;
ClientboundSetEntityDataPacket entityDataPacket = new ClientboundSetEntityDataPacket(playerNpc.getId(), entityData.packDirty());
for(Player nearPlayer : nearPlayers) sendPacket(nearPlayer, entityDataPacket);
}
private void updateEquipment() {
net.minecraft.world.item.ItemStack mainItemStack = serverPlayer.getItemBySlot(net.minecraft.world.entity.EquipmentSlot.MAINHAND);
if(equipmentSlotCache != null && equipmentSlotCache.equals(serverPlayer.getInventory().getContents()) && mainSlotCache == mainItemStack) return;
equipmentSlotCache = NonNullList.create();
equipmentSlotCache.addAll(serverPlayer.getInventory().getContents());
mainSlotCache = mainItemStack;
List<Pair<net.minecraft.world.entity.EquipmentSlot, net.minecraft.world.item.ItemStack>> equipmentList = new ArrayList<>();
for(net.minecraft.world.entity.EquipmentSlot equipmentSlot : net.minecraft.world.entity.EquipmentSlot.values()) {
net.minecraft.world.item.ItemStack itemStack = serverPlayer.getItemBySlot(equipmentSlot);
if(itemStack != null) equipmentList.add(Pair.of(equipmentSlot, itemStack));
}
ClientboundSetEquipmentPacket setEquipmentPacket = new ClientboundSetEquipmentPacket(playerNpc.getId(), equipmentList);
for(Player nearPlayer : nearPlayers) sendPacket(nearPlayer, setEquipmentPacket);
serverPlayer.containerMenu.sendAllDataToRemote();
}
private void setEquipmentVisibility(boolean Visibility) {
List<Pair<net.minecraft.world.entity.EquipmentSlot, net.minecraft.world.item.ItemStack>> equipmentList = new ArrayList<>();
for(net.minecraft.world.entity.EquipmentSlot equipmentSlot : net.minecraft.world.entity.EquipmentSlot.values()) {
net.minecraft.world.item.ItemStack itemStack = Visibility ? serverPlayer.getItemBySlot(equipmentSlot) : null;
equipmentList.add(Pair.of(equipmentSlot, itemStack != null ? itemStack : net.minecraft.world.item.ItemStack.EMPTY));
}
ClientboundSetEquipmentPacket setEquipmentPacket = new ClientboundSetEquipmentPacket(serverPlayer.getId(), equipmentList);
for(Player nearPlayer : nearPlayers) sendPacket(nearPlayer, setEquipmentPacket);
}
private void playAnimation(int Arm) {
ClientboundAnimatePacket animatePacket = new ClientboundAnimatePacket(playerNpc, Arm);
for(Player nearPlayer : nearPlayers) sendPacket(nearPlayer, animatePacket);
}
private byte getFixedRotation(float Yaw) { return (byte) (Yaw * 256f / 360f); }
private Direction getDirection() {
float yaw = seat.getLocation().getYaw();
return (yaw >= 135f || yaw < -135f) ? Direction.NORTH : (yaw >= -135f && yaw < -45f) ? Direction.EAST : (yaw >= -45f && yaw < 45f) ? Direction.SOUTH : yaw >= 45f ? Direction.WEST : Direction.NORTH;
}
private ServerPlayer createNPC() {
MinecraftServer minecraftServer = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel serverLevel = ((CraftWorld) seat.getLocation().getWorld()).getHandle();
GameProfile gameProfile = new GameProfile(UUID.randomUUID(), seatPlayer.getName());
gameProfile.getProperties().putAll(serverPlayer.getGameProfile().getProperties());
ClientInformation clientInformation = serverPlayer.clientInformation();
ServerPlayer sPlayer = new ServerPlayer(minecraftServer, serverLevel, gameProfile, clientInformation);
sPlayer.connection = serverPlayer.connection;
return sPlayer;
}
private void sendPacket(Player Player, Packet<?> Packet) { sendPacket(((CraftPlayer) Player).getHandle(), Packet); }
private void sendPacket(ServerPlayer Player, Packet<?> Packet) { Player.connection.send(Packet); }
@Override
public GSeat getSeat() { return seat; }
@Override
public Player getPlayer() { return seatPlayer; }
@Override
public Pose getPose() { return pose; }
@Override
public String toString() { return seat.toString(); }
}
One must.. imagine Sisyphus happy. Okay, we are at the end of our journey.
The code is actually fairly complex in what it does, and it's going to be tedious to explain each code section at a time, so I'll break it down to a list:
- Player calls /lay
- GSit spawns a fake player
- GSit teleports (using teleport magic witchery) it to the player that did /lay
- GSit sends a message to our game that says "hey theres totally like, half a bed block right at your feet"
- GSit gets that fake player to sleep on that nonexistent bed
- GSit turns the actual player that called it invisible
The conclusion of our journey
Okay, but now to answer the question: why does /lay break? Best guess is, at that last step where "GSit turns the actual player that called it invisible", some other plugin jumped in and messed that up, and it keeps fighting with GSit
Basically
- GSit turns player invisible
- Some other plugin is like "nuh uh"
- GSit gets confused, turns the player invisible again
- This other plugin insists in making the player phase back into reality
Thank you for reading my post, and wishing the readers here a good time ahead!
As a quick note, this is not a dig at active developers working on SRP. I would be ripping my hair out if I had to deal with this problem, not to mention what could even be breaking this. But! I'd love to hear what the plugin developers managing the server think on this, or if they already found the problem and it proved to be much more complex than I had realized here.