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 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.parallel=true
mod_id=sablegravity
mod_version=0.7.4-player-recovery
mod_version=0.7.5-foot-anchor
minecraft_version=1.21.1
neo_version=21.1.228
sable_version=2.0.3+mc1.21.1
@@ -1,15 +1,12 @@
package dev.nightly.sablegravity.mixin;
import dev.nightly.sablegravity.SableGravityState;
import dev.nightly.sablegravity.physics.SableFrameOrientation;
import dev.nightly.sablegravity.physics.SableLocalPlayerPhysics;
import dev.nightly.sablegravity.physics.SableOrientedBounds;
import dev.nightly.sablegravity.physics.SableParentWorldCollision;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import org.joml.Quaterniond;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
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.CallbackInfoReturnable;
import java.util.UUID;
/**
* 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
@@ -26,8 +21,6 @@ import java.util.UUID;
*/
@Mixin(value = Entity.class, priority = 3000)
public abstract class EntityAabbIsolationMixin {
@Unique private Vec3 sablegravity$localFeetBeforeResize;
@Unique private UUID sablegravity$frameBeforeResize;
@Unique private boolean sablegravity$insideMove;
@Inject(method = "collide", at = @At("HEAD"), cancellable = true)
@@ -82,90 +75,8 @@ public abstract class EntityAabbIsolationMixin {
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
private static boolean validFrame(Player player, SubLevel frame) {
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.SableOrientedBounds;
import dev.nightly.sablegravity.physics.SableParentWorldCollision;
import dev.ryanhcode.sable.Sable;
import dev.ryanhcode.sable.sublevel.SubLevel;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
@@ -27,11 +28,19 @@ public abstract class PlayerPoseFitMixin {
) {
Player player = (Player) (Object) this;
SableGravityState state = (SableGravityState) player;
if (!state.sablegravity$isEnabled() || !player.isLocalPlayer()) {
if (!state.sablegravity$isEnabled()
|| (player.level().isClientSide() && !player.isLocalPlayer())) {
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()) {
return;
}
@@ -14,6 +14,7 @@ import net.minecraft.world.phys.Vec3;
import org.joml.Quaterniondc;
import org.joml.Vector3d;
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.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
@@ -66,7 +67,7 @@ public abstract class SableCollisionYawMixin {
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(
method = "transformEntityBoundsCenter",
at = @At("HEAD"),
@@ -80,12 +81,7 @@ public abstract class SableCollisionYawMixin {
Vector3d center,
CallbackInfo ci
) {
if (entity instanceof Player || orientation == null) {
return;
}
GravityEntityState state = (GravityEntityState) entity;
if (state.sablegravity$isInLocalSimulation()
|| state.sablegravity$getFrameId() == null) {
if (!sablegravity$usesFeetAnchoredBody(entity, orientation)) {
return;
}
@@ -117,17 +113,39 @@ public abstract class SableCollisionYawMixin {
float partialTicks,
Quaterniondc orientation
) {
if (!(entity instanceof Player) && orientation != null) {
GravityEntityState state = (GravityEntityState) entity;
if (!state.sablegravity$isInLocalSimulation()
&& state.sablegravity$getFrameId() != null) {
return new Vector3d(
entity.getX(),
entity.getY() - partialTicks,
entity.getZ()
);
}
if (sablegravity$usesFeetAnchoredBody(entity, orientation)) {
return new Vector3d(
entity.getX(),
entity.getY() - partialTicks,
entity.getZ()
);
}
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));
// Sable's custom-orientation OBB is rotated around the eye, while vanilla stores the raw
// entity position at the unrotated feet. Switching the quaternion without rebasing the raw
// position moves the physical OBB bottom by up to twice the eye height. That exact offset
// 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.
// Raw/network position is the invariant physical feet point. The collision mixin rotates
// the OBB center around it, so attaching or changing pose never needs a packet-coordinate
// rebase.
Vec3 physicalFeet;
if (previousFrame != null) {
physicalFeet = SableOrientedBounds.worldFeet(
@@ -238,8 +236,7 @@ public final class SableLocalPlayerPhysics {
if (attached != null
&& !attached.isRemoved()
&& attached.getLevel() == player.level()) {
// Undo the eye-anchor raw-position rebase before vanilla resumes treating position()
// as the physical feet. Otherwise toggling gravity off visibly teleports the player.
// Keep the invariant physical feet when vanilla resumes upright collision.
Vec3 physicalFeet = SableOrientedBounds.worldFeet(
player,
SableFrameOrientation.getFrameOrientation(attached, 1.0F)
@@ -324,7 +321,7 @@ public final class SableLocalPlayerPhysics {
// Incrementally adding lastPose -> logicalPose deltas accumulated interpolation and
// 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
// 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);
LAST_FRAME_MOTION.put(player, actualFrameMotion);
Vector3d inheritedVelocity =
@@ -9,7 +9,7 @@ import org.joml.Quaterniondc;
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
* 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) {
double halfHeight = entity.getBbHeight() * 0.5D;
double anchorOffset = entity.getEyeHeight() - halfHeight;
Vector3d rotatedOffset = orientation.transform(
new Vector3d(0.0D, anchorOffset, 0.0D),
new Vector3d(0.0D, halfHeight, 0.0D),
new Vector3d()
);
Vec3 position = entity.position();
return new Vec3(
position.x - rotatedOffset.x,
position.y + entity.getEyeHeight() - rotatedOffset.y,
position.z - rotatedOffset.z
position.x + rotatedOffset.x,
position.y + rotatedOffset.y,
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) {
Vector3d rotatedEyeOffset = orientation.transform(
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
);
return entity.position();
}
/**
* Converts a physical world-space feet point back to Minecraft's raw entity position for
* Sable's eye-anchored custom orientation contract.
*
* <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>
* Converts physical feet to raw/network position. The orientation is intentionally irrelevant:
* pose and eye-height changes must never move the packet coordinate.
*/
public static Vec3 rawPositionForFeet(
Entity entity,
Vec3 physicalFeet,
Quaterniondc orientation
) {
Vector3d rotatedEyeOffset = orientation.transform(
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
);
return physicalFeet;
}
public static Vec3 localFeet(
@@ -4,7 +4,7 @@ license = "All Rights Reserved"
[[mods]]
modId = "sablegravity"
version = "0.7.4-player-recovery"
version = "0.7.5-foot-anchor"
displayName = "Sable Gravity Local Physics"
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.'''