Anchor crouch collision to player feet

This commit is contained in:
f1den committed 2026-08-11 00:36:42 +03:00
1 parent c186a35c72
commit fceabe9cad
8 files changed
+65 -152

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
# Sable Gravity 0.7.4 # Sable Gravity 0.7.5
Sable Gravity is a multidimensional gravity-frame kernel for moving and arbitrarily rotated Sable Gravity is a multidimensional gravity-frame kernel for moving and arbitrarily rotated
Sable ships. A frame owns a dimension-qualified identity, a local coordinate system, a rigid Sable ships. A frame owns a dimension-qualified identity, a local coordinate system, a rigid
+1 -1
View File
@@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx2G
org.gradle.daemon=false org.gradle.daemon=false
org.gradle.parallel=true org.gradle.parallel=true
mod_id=sablegravity mod_id=sablegravity
mod_version=0.7.4-player-recovery mod_version=0.7.5-foot-anchor
minecraft_version=1.21.1 minecraft_version=1.21.1
neo_version=21.1.228 neo_version=21.1.228
sable_version=2.0.3+mc1.21.1 sable_version=2.0.3+mc1.21.1
@@ -1,15 +1,12 @@
package dev.nightly.sablegravity.mixin; package dev.nightly.sablegravity.mixin;
import dev.nightly.sablegravity.SableGravityState; import dev.nightly.sablegravity.SableGravityState;
import dev.nightly.sablegravity.physics.SableFrameOrientation;
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics; import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
import dev.nightly.sablegravity.physics.SableOrientedBounds;
import dev.nightly.sablegravity.physics.SableParentWorldCollision; import dev.nightly.sablegravity.physics.SableParentWorldCollision;
import dev.ryanhcode.sable.sublevel.SubLevel; import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import org.joml.Quaterniond;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@@ -17,8 +14,6 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.UUID;
/** /**
* Keeps Entity.bb as metadata/broadphase only while arbitrary gravity is active. This mixin has * Keeps Entity.bb as metadata/broadphase only while arbitrary gravity is active. This mixin has
* higher priority than Sable's Entity mixin so the semantic-flag restoration point is inserted * higher priority than Sable's Entity mixin so the semantic-flag restoration point is inserted
@@ -26,8 +21,6 @@ import java.util.UUID;
*/ */
@Mixin(value = Entity.class, priority = 3000) @Mixin(value = Entity.class, priority = 3000)
public abstract class EntityAabbIsolationMixin { public abstract class EntityAabbIsolationMixin {
@Unique private Vec3 sablegravity$localFeetBeforeResize;
@Unique private UUID sablegravity$frameBeforeResize;
@Unique private boolean sablegravity$insideMove; @Unique private boolean sablegravity$insideMove;
@Inject(method = "collide", at = @At("HEAD"), cancellable = true) @Inject(method = "collide", at = @At("HEAD"), cancellable = true)
@@ -82,90 +75,8 @@ public abstract class EntityAabbIsolationMixin {
sablegravity$insideMove = false; sablegravity$insideMove = false;
} }
@Inject(method = "refreshDimensions", at = @At("HEAD"))
private void sablegravity$captureFeetBeforeResize(CallbackInfo ci) {
Entity self = (Entity) (Object) this;
if (!(self instanceof Player player) || !usesOrientedBody(player)) {
sablegravity$clearResizeAnchor();
return;
}
SubLevel frame = currentFrame(player);
if (!validFrame(player, frame)) {
sablegravity$clearResizeAnchor();
return;
}
// A pose transition belongs to the Sable frame, not to parent-world space. Anchoring the
// transient resize to a world point lets a simultaneously moving/rotating frame turn
// crouch into a visible teleport or tangent slide. Store the invariant local feet point.
Quaterniond orientation = SableFrameOrientation.getFrameOrientation(frame, 1.0F);
Vec3 worldFeet = SableOrientedBounds.worldFeet(player, orientation);
sablegravity$localFeetBeforeResize = frame.logicalPose()
.transformPositionInverse(worldFeet);
sablegravity$frameBeforeResize = frame.getUniqueId();
}
@Inject(method = "refreshDimensions", at = @At("TAIL"))
private void sablegravity$restoreFeetAfterResize(CallbackInfo ci) {
Entity self = (Entity) (Object) this;
if (!(self instanceof Player player)
|| sablegravity$localFeetBeforeResize == null) {
sablegravity$clearResizeAnchor();
return;
}
SubLevel frame = currentFrame(player);
if (!validFrame(player, frame)
|| !frame.getUniqueId().equals(sablegravity$frameBeforeResize)) {
sablegravity$clearResizeAnchor();
return;
}
Quaterniond orientation = SableFrameOrientation.getFrameOrientation(frame, 1.0F);
Vec3 physicalFeet = frame.logicalPose().transformPosition(
sablegravity$localFeetBeforeResize
);
Vec3 rawPosition = SableOrientedBounds.rawPositionForFeet(
player,
physicalFeet,
orientation
);
if (Double.isFinite(rawPosition.x)
&& Double.isFinite(rawPosition.y)
&& Double.isFinite(rawPosition.z)) {
player.setPos(rawPosition);
}
sablegravity$clearResizeAnchor();
}
@Unique
private static boolean usesOrientedBody(Player player) {
if (!((SableGravityState) player).sablegravity$isEnabled()) {
return false;
}
return player.isLocalPlayer();
}
@Unique
private static SubLevel currentFrame(Player player) {
if (player.isLocalPlayer()) {
SubLevel attached = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
if (validFrame(player, attached)) {
return attached;
}
}
return null;
}
@Unique @Unique
private static boolean validFrame(Player player, SubLevel frame) { private static boolean validFrame(Player player, SubLevel frame) {
return frame != null && !frame.isRemoved() && frame.getLevel() == player.level(); return frame != null && !frame.isRemoved() && frame.getLevel() == player.level();
} }
@Unique
private void sablegravity$clearResizeAnchor() {
sablegravity$localFeetBeforeResize = null;
sablegravity$frameBeforeResize = null;
}
} }
@@ -6,6 +6,7 @@ import dev.nightly.sablegravity.physics.SableLocalCollisionProbe;
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics; import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
import dev.nightly.sablegravity.physics.SableOrientedBounds; import dev.nightly.sablegravity.physics.SableOrientedBounds;
import dev.nightly.sablegravity.physics.SableParentWorldCollision; import dev.nightly.sablegravity.physics.SableParentWorldCollision;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel; import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.Pose;
@@ -27,11 +28,19 @@ public abstract class PlayerPoseFitMixin {
) { ) {
Player player = (Player) (Object) this; Player player = (Player) (Object) this;
SableGravityState state = (SableGravityState) player; SableGravityState state = (SableGravityState) player;
if (!state.sablegravity$isEnabled() || !player.isLocalPlayer()) { if (!state.sablegravity$isEnabled()
|| (player.level().isClientSide() && !player.isLocalPlayer())) {
return; return;
} }
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player); // The local client owns movement, but the server still owns pose/entity-data
// synchronization. Its upright parent-world fit test sees the rotated ship as unrelated
// terrain and rejects CROUCHING even though the exact ship-local OBB fits. Use the same
// frame-aware fit contract on both authoritative endpoints; remote client replicas only
// consume the synchronized pose and do not run this branch.
SubLevel frame = player.isLocalPlayer()
? SableLocalPlayerPhysics.getAttachedFrameIfPresent(player)
: Sable.HELPER.getTrackingSubLevel(player);
if (frame == null || frame.isRemoved() || frame.getLevel() != player.level()) { if (frame == null || frame.isRemoved() || frame.getLevel() != player.level()) {
return; return;
} }
@@ -14,6 +14,7 @@ import net.minecraft.world.phys.Vec3;
import org.joml.Quaterniondc; import org.joml.Quaterniondc;
import org.joml.Vector3d; import org.joml.Vector3d;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
@@ -66,7 +67,7 @@ public abstract class SableCollisionYawMixin {
return SubLevelEntityCollision.getHitBoxYaw(pose); return SubLevelEntityCollision.getHitBoxYaw(pose);
} }
/** Rotates non-player bodies around their invariant raw feet position, never around the eye. */ /** Rotates gravity bodies around their invariant raw/network feet position, never the eye. */
@Inject( @Inject(
method = "transformEntityBoundsCenter", method = "transformEntityBoundsCenter",
at = @At("HEAD"), at = @At("HEAD"),
@@ -80,12 +81,7 @@ public abstract class SableCollisionYawMixin {
Vector3d center, Vector3d center,
CallbackInfo ci CallbackInfo ci
) { ) {
if (entity instanceof Player || orientation == null) { if (!sablegravity$usesFeetAnchoredBody(entity, orientation)) {
return;
}
GravityEntityState state = (GravityEntityState) entity;
if (state.sablegravity$isInLocalSimulation()
|| state.sablegravity$getFrameId() == null) {
return; return;
} }
@@ -117,17 +113,39 @@ public abstract class SableCollisionYawMixin {
float partialTicks, float partialTicks,
Quaterniondc orientation Quaterniondc orientation
) { ) {
if (!(entity instanceof Player) && orientation != null) { if (sablegravity$usesFeetAnchoredBody(entity, orientation)) {
GravityEntityState state = (GravityEntityState) entity; return new Vector3d(
if (!state.sablegravity$isInLocalSimulation() entity.getX(),
&& state.sablegravity$getFrameId() != null) { entity.getY() - partialTicks,
return new Vector3d( entity.getZ()
entity.getX(), );
entity.getY() - partialTicks,
entity.getZ()
);
}
} }
return companion.getFeetPos(entity, partialTicks, orientation); return companion.getFeetPos(entity, partialTicks, orientation);
} }
@Unique
private static boolean sablegravity$usesFeetAnchoredBody(
Entity entity,
Quaterniondc orientation
) {
if (orientation == null) {
return false;
}
GravityEntityState state = (GravityEntityState) entity;
if (state.sablegravity$isInLocalSimulation()) {
return false;
}
if (entity instanceof Player player) {
if (!player.isLocalPlayer()
|| !((SableGravityState) player).sablegravity$isEnabled()) {
return false;
}
SubLevel frame = SableLocalPlayerPhysics.getAttachedFrameIfPresent(player);
return frame != null && !frame.isRemoved() && frame.getLevel() == player.level();
}
return state.sablegravity$getFrameId() != null;
}
} }
@@ -130,11 +130,9 @@ public final class SableLocalPlayerPhysics {
) { ) {
SubLevel previousFrame = validFrame(player, ATTACHED_FRAMES.get(player)); SubLevel previousFrame = validFrame(player, ATTACHED_FRAMES.get(player));
// Sable's custom-orientation OBB is rotated around the eye, while vanilla stores the raw // Raw/network position is the invariant physical feet point. The collision mixin rotates
// entity position at the unrotated feet. Switching the quaternion without rebasing the raw // the OBB center around it, so attaching or changing pose never needs a packet-coordinate
// position moves the physical OBB bottom by up to twice the eye height. That exact offset // rebase.
// was then reported as inheritedMotion (2.32 blocks in the supplied log) and launched the
// player. Preserve the physical feet point and change only the raw eye-anchor position.
Vec3 physicalFeet; Vec3 physicalFeet;
if (previousFrame != null) { if (previousFrame != null) {
physicalFeet = SableOrientedBounds.worldFeet( physicalFeet = SableOrientedBounds.worldFeet(
@@ -238,8 +236,7 @@ public final class SableLocalPlayerPhysics {
if (attached != null if (attached != null
&& !attached.isRemoved() && !attached.isRemoved()
&& attached.getLevel() == player.level()) { && attached.getLevel() == player.level()) {
// Undo the eye-anchor raw-position rebase before vanilla resumes treating position() // Keep the invariant physical feet when vanilla resumes upright collision.
// as the physical feet. Otherwise toggling gravity off visibly teleports the player.
Vec3 physicalFeet = SableOrientedBounds.worldFeet( Vec3 physicalFeet = SableOrientedBounds.worldFeet(
player, player,
SableFrameOrientation.getFrameOrientation(attached, 1.0F) SableFrameOrientation.getFrameOrientation(attached, 1.0F)
@@ -324,7 +321,7 @@ public final class SableLocalPlayerPhysics {
// Incrementally adding lastPose -> logicalPose deltas accumulated interpolation and // Incrementally adding lastPose -> logicalPose deltas accumulated interpolation and
// client/server round-off as a small tangent displacement every tick; the player had zero // client/server round-off as a small tangent displacement every tick; the player had zero
// local X/Z velocity but still slid off the deck. The local feet coordinate is the source // local X/Z velocity but still slid off the deck. The local feet coordinate is the source
// of truth, so converge the raw eye-anchor position to its current logical-pose image. // of truth, so converge raw/network feet to its current logical-pose image.
Vec3 actualFrameMotion = applyCorrectedFrameMotion(player, frameMotion); Vec3 actualFrameMotion = applyCorrectedFrameMotion(player, frameMotion);
LAST_FRAME_MOTION.put(player, actualFrameMotion); LAST_FRAME_MOTION.put(player, actualFrameMotion);
Vector3d inheritedVelocity = Vector3d inheritedVelocity =
@@ -9,7 +9,7 @@ import org.joml.Quaterniondc;
import org.joml.Vector3d; import org.joml.Vector3d;
/** /**
* Canonical dimensions and the exact eye-anchored OBB used by Sable gravity entities. * Canonical dimensions for the feet-anchored OBB used by Sable gravity entities.
* *
* Minecraft's AABB type cannot encode an arbitrary orientation. The exact body is therefore * Minecraft's AABB type cannot encode an arbitrary orientation. The exact body is therefore
* represented by a center, half extents and quaternion; {@link #worldEnvelope(Entity, Quaterniondc)} * represented by a center, half extents and quaternion; {@link #worldEnvelope(Entity, Quaterniondc)}
@@ -32,58 +32,36 @@ public final class SableOrientedBounds {
); );
} }
/** Exact world-space OBB center matching Sable's transformEntityBoundsCenter contract. */ /** Exact world-space OBB center: raw entity position is always the physical feet point. */
public static Vec3 worldCenter(Entity entity, Quaterniondc orientation) { public static Vec3 worldCenter(Entity entity, Quaterniondc orientation) {
double halfHeight = entity.getBbHeight() * 0.5D; double halfHeight = entity.getBbHeight() * 0.5D;
double anchorOffset = entity.getEyeHeight() - halfHeight;
Vector3d rotatedOffset = orientation.transform( Vector3d rotatedOffset = orientation.transform(
new Vector3d(0.0D, anchorOffset, 0.0D), new Vector3d(0.0D, halfHeight, 0.0D),
new Vector3d() new Vector3d()
); );
Vec3 position = entity.position(); Vec3 position = entity.position();
return new Vec3( return new Vec3(
position.x - rotatedOffset.x, position.x + rotatedOffset.x,
position.y + entity.getEyeHeight() - rotatedOffset.y, position.y + rotatedOffset.y,
position.z - rotatedOffset.z position.z + rotatedOffset.z
); );
} }
/** Physical feet point after rotating the canonical body around the eye anchor. */ /** Raw/network position and physical feet are the same invariant point. */
public static Vec3 worldFeet(Entity entity, Quaterniondc orientation) { public static Vec3 worldFeet(Entity entity, Quaterniondc orientation) {
Vector3d rotatedEyeOffset = orientation.transform( return entity.position();
new Vector3d(0.0D, entity.getEyeHeight(), 0.0D),
new Vector3d()
);
Vec3 position = entity.position();
return new Vec3(
position.x - rotatedEyeOffset.x,
position.y + entity.getEyeHeight() - rotatedEyeOffset.y,
position.z - rotatedEyeOffset.z
);
} }
/** /**
* Converts a physical world-space feet point back to Minecraft's raw entity position for * Converts physical feet to raw/network position. The orientation is intentionally irrelevant:
* Sable's eye-anchored custom orientation contract. * pose and eye-height changes must never move the packet coordinate.
*
* <p>Sable rotates a custom entity OBB around the eye. Therefore {@code Entity.position()}
* is no longer the physical bottom-center of the OBB. Keeping the feet fixed while changing
* orientation requires moving the raw position by {@code Q * eyeOffset - worldEyeOffset}.</p>
*/ */
public static Vec3 rawPositionForFeet( public static Vec3 rawPositionForFeet(
Entity entity, Entity entity,
Vec3 physicalFeet, Vec3 physicalFeet,
Quaterniondc orientation Quaterniondc orientation
) { ) {
Vector3d rotatedEyeOffset = orientation.transform( return physicalFeet;
new Vector3d(0.0D, entity.getEyeHeight(), 0.0D),
new Vector3d()
);
return new Vec3(
physicalFeet.x + rotatedEyeOffset.x,
physicalFeet.y + rotatedEyeOffset.y - entity.getEyeHeight(),
physicalFeet.z + rotatedEyeOffset.z
);
} }
public static Vec3 localFeet( public static Vec3 localFeet(
@@ -4,7 +4,7 @@ license = "All Rights Reserved"
[[mods]] [[mods]]
modId = "sablegravity" modId = "sablegravity"
version = "0.7.4-player-recovery" version = "0.7.5-foot-anchor"
displayName = "Sable Gravity Local Physics" displayName = "Sable Gravity Local Physics"
authors = "Nightly" authors = "Nightly"
description = '''Sable ship-local player gravity and collision. Non-player entities remain on Sable's native lifecycle while the universal frame layer is rebuilt safely.''' description = '''Sable ship-local player gravity and collision. Non-player entities remain on Sable's native lifecycle while the universal frame layer is rebuilt safely.'''