Seat rework

This commit is contained in:
Darto KLoning
2026-10-03 10:54:04 +07:00
parent 98e9869daa
commit 258354b7d9
4 changed files with 66 additions and 182 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx4G
org.gradle.parallel=true
org.gradle.caching=true
mod_version=4.0.9
mod_version=4.0.10
maven_group=earth.terrarium.handcrafted
archives_base_name=handcrafted
@@ -10,29 +10,20 @@ import net.minecraft.world.phys.AABB;
public interface SittableBlock {
default boolean sitOn(Level level, BlockPos pos, Player player, Direction dir) {
// 26.2 evaluates empty-hand block interaction client-side first. Returning
// success here keeps the interaction authoritative on the server while
// ensuring the use packet is sent for chairs/benches/couches.
// Keep seating server-authoritative. On the client, report a handled
// interaction so Minecraft sends/keeps the block-use interaction path.
if (level.isClientSide()) {
return true;
}
if (!Seat.SITTING_POSITIONS.get(level.dimension()).contains(pos)) {
Seat entity = Seat.of(level, pos, dir);
if (level.addFreshEntity(entity)) {
// Do not mount in the same server tick in which the invisible
// Seat is spawned. In 26.2 the passenger packet can otherwise
// reach the client before the entity-spawn packet, producing
// "Received passengers for unknown entity" and an immediate
// client/server mount desync. Queue the rider and let Seat.tick()
// mount it after the entity has entered normal tracking.
Seat.SITTING_POSITIONS.put(level.dimension(), pos.immutable());
entity.queueRider(player);
return true;
} else {
entity.removeSeat();
return Seat.sit(player, pos, this.getSeatHeight(level.getBlockState(pos)), dir);
}
}
return false;
default double getSeatHeight(BlockState state) {
// Preserve Handcrafted's old rider height: 75% of the configured seat
// AABB plus 0.2 blocks, but place the zero-sized vehicle itself there.
// This matches the modern, working furniture-seat pattern and avoids
// custom passenger attachment/bounding-box synchronization.
return this.getSeatSize(state).getYsize() * 0.75D + 0.2D;
}
AABB getSeatSize(BlockState state);
@@ -1,72 +1,70 @@
package earth.terrarium.handcrafted.common.entities;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import earth.terrarium.handcrafted.common.blocks.base.SittableBlock;
import earth.terrarium.handcrafted.common.registry.ModEntityTypes;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.util.Mth;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.vehicle.DismountHelper;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.entity.EntityInLevelCallback;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
public class Seat extends Entity {
public static final Multimap<ResourceKey<Level>, BlockPos> SITTING_POSITIONS = ArrayListMultimap.create();
private AABB shape;
private boolean remove;
private boolean canRotate;
private Entity pendingRider;
private int mountDelayTicks;
public Seat(EntityType<? extends Entity> entityType, Level level) {
super(entityType, level);
this.setLevelCallback(EntityInLevelCallback.NULL);
}
public Seat(Level level, AABB shape) {
super(ModEntityTypes.SEAT.get(), level);
this.shape = copyAABB(shape);
}
public static Seat of(Level level, BlockPos pos, Direction dir) {
BlockState state = level.getBlockState(pos);
AABB shape = new AABB(pos);
if (state.getBlock() instanceof SittableBlock seat) {
shape = seat.getSeatSize(state);
}
Seat entity = new Seat(level, shape);
private Seat(Level level, BlockPos pos, double seatHeight, Direction dir) {
this(ModEntityTypes.SEAT.get(), level);
this.setPos(Vec3.atBottomCenterOf(pos).add(0, seatHeight, 0));
if (dir != null) {
entity.setYRot(dir.toYRot());
this.setRot(dir.toYRot(), 0);
} else {
entity.canRotate = true;
this.setRot(0, 0);
this.canRotate = true;
}
entity.setPos(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5);
return entity;
}
private static AABB copyAABB(AABB aabb) {
return new AABB(aabb.minX, aabb.minY, aabb.minZ, aabb.maxX, aabb.maxY, aabb.maxZ);
public static boolean sit(Player player, BlockPos pos, double seatHeight, Direction dir) {
Level level = player.level();
if (!level.isClientSide() && availableAt(level, pos)) {
Seat seat = new Seat(level, pos, seatHeight, dir);
level.addFreshEntity(seat);
if (player.startRiding(seat)) {
return true;
}
seat.discard();
}
return false;
}
// Use Minecraft's native entity spawn packet on 26.2.
// The old custom ClientboundAddEntityPacket path from 1.21.x can race the
// passenger packet, leaving the client with an unknown vehicle entity.
public static boolean availableAt(Level level, BlockPos pos) {
return level.getEntitiesOfClass(Seat.class, new AABB(pos)).isEmpty();
}
@Override
public void tick() {
super.tick();
if (!this.level().isClientSide()) {
BlockPos pos = this.blockPosition();
if (this.getPassengers().isEmpty() || !(this.level().getBlockState(pos).getBlock() instanceof SittableBlock)) {
this.discard();
this.level().updateNeighbourForOutputSignal(pos, this.level().getBlockState(pos).getBlock());
}
}
}
@Override
public boolean hurtServer(ServerLevel level, DamageSource source, float damage) {
@@ -79,113 +77,35 @@ public class Seat extends Entity {
}
@Override
public boolean isVehicle() {
return true;
}
// Minecraft 26.2 no longer treats a generic Entity subclass as a
// passenger-capable vehicle merely because isVehicle() returns true.
// Explicitly opt this invisible seat entity into accepting one rider.
@Override
protected boolean couldAcceptPassenger() {
return true;
protected void addPassenger(Entity entity) {
super.addPassenger(entity);
if (!canRotate) entity.setYRot(this.getYRot());
}
@Override
protected boolean canAddPassenger(Entity passenger) {
return this.getPassengers().isEmpty();
public void onPassengerTurned(Entity entity) {
if (!canRotate) clampPassengerYaw(entity);
}
@Override
public boolean shouldRender(double x, double y, double z) {
return false;
private void clampPassengerYaw(Entity entity) {
entity.setYBodyRot(this.getYRot());
float wrapped = Mth.wrapDegrees(entity.getYRot() - this.getYRot());
float clamped = Mth.clamp(wrapped, -105.0F, 105.0F);
entity.yRotO += clamped - wrapped;
entity.setYRot(entity.getYRot() + clamped - wrapped);
entity.setYHeadRot(entity.getYRot());
}
@Override
public Vec3 getDismountLocationForPassenger(LivingEntity passenger) {
Direction facing = this.getDirection();
for (Direction offset : new Direction[]{facing, facing.getClockWise(), facing.getCounterClockWise(), facing.getOpposite()}) {
Vec3 safeLocation = DismountHelper.findSafeDismountLocation(passenger.getType(), this.level(), this.blockPosition().relative(offset), false);
if (safeLocation != null) {
return safeLocation.add(0, 0.25, 0);
}
Direction facing = canRotate ? passenger.getDirection() : this.getDirection();
for (Direction side : new Direction[]{facing, facing.getClockWise(), facing.getCounterClockWise(), facing.getOpposite()}) {
Vec3 safe = DismountHelper.findSafeDismountLocation(passenger.getType(), this.level(), this.blockPosition().relative(side), false);
if (safe != null) return safe.add(0, 0.25, 0);
}
return super.getDismountLocationForPassenger(passenger);
}
public void queueRider(Entity rider) {
this.pendingRider = rider;
// One complete entity tick is enough for ServerEntity tracking to send
// the add-entity packet before we generate a passengers packet.
this.mountDelayTicks = 1;
}
@Override
public void tick() {
super.tick();
if (!level().isClientSide()) {
if (!(this.level().getBlockState(this.blockPosition()).getBlock() instanceof SittableBlock) || remove) {
removeSeat();
return;
}
if (pendingRider != null) {
if (mountDelayTicks-- <= 0) {
Entity rider = pendingRider;
pendingRider = null;
if (!rider.isRemoved() && rider.level() == this.level() && rider.startRiding(this)) {
return;
}
removeSeat();
}
}
}
}
@Override
protected void removePassenger(Entity passenger) {
super.removePassenger(passenger);
if (!level().isClientSide() && this.getPassengers().isEmpty()) {
remove = true;
}
}
public void removeSeat() {
SITTING_POSITIONS.get(this.level().dimension()).remove(this.blockPosition());
this.discard();
}
@Override
protected Vec3 getPassengerAttachmentPoint(Entity entity, EntityDimensions dimensions, float partialTick) {
if (shape == null) {
return super.getPassengerAttachmentPoint(entity, dimensions, partialTick);
}
return new Vec3(0, (float) (shape.getYsize() * 0.75) + 0.2f, 0);
}
protected void clampRotation(Entity entityToUpdate) {
entityToUpdate.setYBodyRot(this.getYRot());
float f = Mth.wrapDegrees(entityToUpdate.getYRot() - this.getYRot());
float g = Mth.clamp(f, -105.0F, 105.0F);
entityToUpdate.yRotO += g - f;
entityToUpdate.setYRot(entityToUpdate.getYRot() + g - f);
entityToUpdate.setYHeadRot(entityToUpdate.getYRot());
}
@Override
public void onPassengerTurned(Entity entityToUpdate) {
if (!canRotate) {
this.clampRotation(entityToUpdate);
}
}
@Override
public void setLevelCallback(EntityInLevelCallback levelCallback) {
super.setLevelCallback(new WrappedLevelCallBack(levelCallback));
}
@Override
protected void defineSynchedData(SynchedEntityData.Builder builder) {}
@@ -194,31 +114,4 @@ public class Seat extends Entity {
@Override
protected void addAdditionalSaveData(ValueOutput output) {}
private class WrappedLevelCallBack implements EntityInLevelCallback {
private final EntityInLevelCallback callback;
public WrappedLevelCallBack(EntityInLevelCallback callback) {
this.callback = callback;
}
@Override
public void onMove() {
if (callback != null) {
callback.onMove();
if (level().getBlockState(blockPosition()).getBlock() instanceof SittableBlock seat) {
shape = seat.getSeatSize(level().getBlockState(blockPosition()));
}
} else {
Seat.this.shape = null;
}
}
@Override
public void onRemove(RemovalReason reason) {
if (callback != null) {
callback.onRemove(reason);
}
}
}
}
@@ -18,7 +18,7 @@ public class ModEntityTypes {
public static final RegistryEntry<EntityType<Seat>> SEAT = ENTITY_TYPES.register("seat", () ->
EntityType.Builder.<Seat>of(Seat::new, MobCategory.MISC)
.sized(1, 1)
.sized(0, 0)
.noSave()
.fireImmune()
.noSummon()