diff --git a/README.md b/README.md index fbd94ef..b80eb83 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/gradle.properties b/gradle.properties index 00853cf..507c143 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/java/dev/nightly/sablegravity/mixin/EntityAabbIsolationMixin.java b/src/main/java/dev/nightly/sablegravity/mixin/EntityAabbIsolationMixin.java index bff2c36..16263c8 100644 --- a/src/main/java/dev/nightly/sablegravity/mixin/EntityAabbIsolationMixin.java +++ b/src/main/java/dev/nightly/sablegravity/mixin/EntityAabbIsolationMixin.java @@ -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; - } } diff --git a/src/main/java/dev/nightly/sablegravity/mixin/PlayerPoseFitMixin.java b/src/main/java/dev/nightly/sablegravity/mixin/PlayerPoseFitMixin.java index 1e2e412..51acfae 100644 --- a/src/main/java/dev/nightly/sablegravity/mixin/PlayerPoseFitMixin.java +++ b/src/main/java/dev/nightly/sablegravity/mixin/PlayerPoseFitMixin.java @@ -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; } diff --git a/src/main/java/dev/nightly/sablegravity/mixin/SableCollisionYawMixin.java b/src/main/java/dev/nightly/sablegravity/mixin/SableCollisionYawMixin.java index 89f6dd0..768c65a 100644 --- a/src/main/java/dev/nightly/sablegravity/mixin/SableCollisionYawMixin.java +++ b/src/main/java/dev/nightly/sablegravity/mixin/SableCollisionYawMixin.java @@ -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; + } } diff --git a/src/main/java/dev/nightly/sablegravity/physics/SableLocalPlayerPhysics.java b/src/main/java/dev/nightly/sablegravity/physics/SableLocalPlayerPhysics.java index 19517b6..ff31ac5 100644 --- a/src/main/java/dev/nightly/sablegravity/physics/SableLocalPlayerPhysics.java +++ b/src/main/java/dev/nightly/sablegravity/physics/SableLocalPlayerPhysics.java @@ -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 = diff --git a/src/main/java/dev/nightly/sablegravity/physics/SableOrientedBounds.java b/src/main/java/dev/nightly/sablegravity/physics/SableOrientedBounds.java index 82fba0b..3069283 100644 --- a/src/main/java/dev/nightly/sablegravity/physics/SableOrientedBounds.java +++ b/src/main/java/dev/nightly/sablegravity/physics/SableOrientedBounds.java @@ -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. - * - *
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}.
+ * 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( diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index a6d4e03..aa2224e 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -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.'''